mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 05:54:56 +02:00
[eric] security: make the 9router state dir owner-only, its db.json held live tokens at 0644
This commit is contained in:
@@ -17,6 +17,7 @@ import os
|
||||
import secrets
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
@@ -115,6 +116,25 @@ def p_nine_router_data_dir() -> str:
|
||||
return os.path.join(os.path.expanduser("~"), ".9router")
|
||||
|
||||
|
||||
def harden_data_dir_permissions() -> None:
|
||||
"""Make the 9Router state dir owner-only. Its db.json holds live subscription access AND refresh
|
||||
tokens in plaintext and 9Router writes it 0644, so on a shared machine any other local account
|
||||
can read them. We tighten the DIRECTORY rather than the file because 9Router rewrites db.json on
|
||||
every token refresh, which would drop a chmod on the file itself within the hour."""
|
||||
if os.name == "nt":
|
||||
return
|
||||
data_dir = p_nine_router_data_dir()
|
||||
try:
|
||||
if not os.path.isdir(data_dir):
|
||||
return
|
||||
current = stat.S_IMODE(os.stat(data_dir).st_mode)
|
||||
if current & 0o077:
|
||||
os.chmod(data_dir, 0o700)
|
||||
logger.info("tightened 9router data dir from %s to 0700", oct(current))
|
||||
except OSError:
|
||||
logger.warning("could not tighten 9router data dir permissions", exc_info=True)
|
||||
|
||||
|
||||
p_cli_token_cache: str | None = None
|
||||
|
||||
|
||||
@@ -353,6 +373,7 @@ async def ensure_running():
|
||||
p_start_lock = asyncio.Lock()
|
||||
async with p_start_lock:
|
||||
await p_ensure_running_impl()
|
||||
harden_data_dir_permissions()
|
||||
# Arm both healers the moment the router becomes a live dependency; users who never route through it never spawn them.
|
||||
if is_running():
|
||||
start_watchdog()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""9Router's state dir must be owner-only.
|
||||
|
||||
Its db.json carries live subscription access AND refresh tokens in plaintext, and 9Router writes
|
||||
that file 0644. On a shared machine every other local account could read them. We tighten the
|
||||
directory rather than the file because 9Router rewrites db.json on every token refresh, so a chmod
|
||||
on the file itself would be undone within the hour.
|
||||
|
||||
Run:
|
||||
cd backend && .venv/bin/python -m pytest tests/test_router_data_dir_permissions.py -v
|
||||
"""
|
||||
|
||||
import os
|
||||
import stat
|
||||
|
||||
import pytest
|
||||
|
||||
import backend.apps.nine_router.process as process
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def p_data_dir(tmp_path, monkeypatch):
|
||||
d = tmp_path / "9router"
|
||||
d.mkdir()
|
||||
monkeypatch.setenv("DATA_DIR", str(d))
|
||||
return d
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits only")
|
||||
def test_group_and_world_readable_dir_is_tightened(p_data_dir):
|
||||
os.chmod(p_data_dir, 0o755)
|
||||
process.harden_data_dir_permissions()
|
||||
assert stat.S_IMODE(os.stat(p_data_dir).st_mode) == 0o700
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits only")
|
||||
def test_tokens_are_unreadable_by_others_afterwards(p_data_dir):
|
||||
"""The property that actually matters, stated as the attacker sees it: no bit outside the owner."""
|
||||
os.chmod(p_data_dir, 0o755)
|
||||
(p_data_dir / "db.json").write_text('{"providerConnections":[]}')
|
||||
process.harden_data_dir_permissions()
|
||||
assert stat.S_IMODE(os.stat(p_data_dir).st_mode) & 0o077 == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits only")
|
||||
def test_already_tight_dir_is_left_alone(p_data_dir):
|
||||
os.chmod(p_data_dir, 0o700)
|
||||
before = os.stat(p_data_dir).st_mtime_ns
|
||||
process.harden_data_dir_permissions()
|
||||
assert stat.S_IMODE(os.stat(p_data_dir).st_mode) == 0o700
|
||||
assert os.stat(p_data_dir).st_mtime_ns == before
|
||||
|
||||
|
||||
def test_missing_dir_does_not_raise(tmp_path, monkeypatch):
|
||||
"""Runs before 9Router has ever started, so the dir legitimately may not exist yet."""
|
||||
monkeypatch.setenv("DATA_DIR", str(tmp_path / "nope"))
|
||||
process.harden_data_dir_permissions()
|
||||
Reference in New Issue
Block a user