From 1e59cc85da10caebf7736e16b3c5e45d1ef94fd6 Mon Sep 17 00:00:00 2001 From: SUJALMU2004 Date: Mon, 18 May 2026 23:22:57 +0530 Subject: [PATCH] fix: harden IPC limits and secure file handling - Increased Unix socket server limit to 16MB to allow agents to inject large JS/HTML payloads without crashing. - Maintained Windows TCP loopback at 64KB limit to prevent pre-auth local connection DoS. - Switched socket unlink to use EAFP (try/except OSError) instead of os.path.exists to fix a crash caused by broken symlinks. - Added a safe_open_write helper with O_NOFOLLOW and applied it to PID/LOG files to patch a CWE-61 symlink overwrite vulnerability in shared /tmp directories. --- src/browser_harness/_ipc.py | 22 +++++++++++++++++++--- src/browser_harness/daemon.py | 15 ++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/browser_harness/_ipc.py b/src/browser_harness/_ipc.py index 2d265766..19315daa 100644 --- a/src/browser_harness/_ipc.py +++ b/src/browser_harness/_ipc.py @@ -59,6 +59,19 @@ def _read_port_file(name): return None, None +def safe_open_write(path, append=False): + """Securely open a file for writing, refusing to follow symlinks where supported.""" + flags = os.O_WRONLY | os.O_CREAT + flags |= os.O_APPEND if append else os.O_TRUNC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + fd = os.open(str(path), flags, 0o600) + return open(fd, "a" if append else "w") + except OSError: + raise RuntimeError(f"Refusing to write to {path} as it may be a symlink attack.") + + def sock_addr(name): # display-only, used in log lines if not IS_WINDOWS: return str(_sock_path(name)) port, _ = _read_port_file(name) @@ -163,14 +176,17 @@ async def serve(name, handler): global _server_token if not IS_WINDOWS: path = str(_sock_path(name)) - if os.path.exists(path): os.unlink(path) + try: os.unlink(path) + except OSError: pass # umask 0o077 makes bind() create the socket as 0600 — no TOCTOU window before chmod. old_umask = os.umask(0o077) - try: server = await asyncio.start_unix_server(handler, path=path) + try: server = await asyncio.start_unix_server(handler, path=path, limit=1024 * 1024 * 16) finally: os.umask(old_umask) _server_token = None async with server: await asyncio.Event().wait() return + # Windows TCP loopback: keep the default 64KB limit to prevent unauthenticated + # local clients from forcing large pre-auth memory allocations (DoS). server = await asyncio.start_server(handler, "127.0.0.1", 0) port = server.sockets[0].getsockname()[1] _server_token = secrets.token_hex(32) @@ -194,4 +210,4 @@ def expected_token(): def cleanup_endpoint(name): # best-effort; silent if already gone p = _sock_path(name) if not IS_WINDOWS else port_path(name) try: p.unlink() - except FileNotFoundError: pass + except OSError: pass diff --git a/src/browser_harness/daemon.py b/src/browser_harness/daemon.py index 0f0f2555..aef3c2ba 100644 --- a/src/browser_harness/daemon.py +++ b/src/browser_harness/daemon.py @@ -70,7 +70,11 @@ def _load_env_file(p): def log(msg): - open(LOG, "a").write(f"{msg}\n") + try: + with ipc.safe_open_write(LOG, append=True) as f: + f.write(f"{msg}\n") + except RuntimeError: + pass async def _silent(coro): @@ -405,8 +409,13 @@ def already_running(): if already_running(): print(f"daemon already running on {SOCK}", file=sys.stderr) sys.exit(0) - open(LOG, "w").close() - open(PID, "w").write(str(os.getpid())) + try: + ipc.safe_open_write(LOG).close() + with ipc.safe_open_write(PID) as f: + f.write(str(os.getpid())) + except RuntimeError as e: + print(f"fatal: {e}", file=sys.stderr) + sys.exit(1) try: asyncio.run(main()) except KeyboardInterrupt: