fix(automation): TCP automation requires a session token

--http (headless and GUI-hosted) and --serve --port answered anything
that reached the loopback port, and the REST responses allowed every
web origin. Loopback is shared by every process and user on the
machine, so the port proved nothing.

- Every REST request now needs Authorization: Bearer <token>, and every
  --serve --port request line a "token" field; otherwise 401 /
  unauthorized (the socket is closed).
- The token is OCS_API_TOKEN when the launcher sets it, else 32 random
  bytes printed on stderr at startup; compared in constant time.
- No CORS headers are sent, so a web page cannot read an answer.
- stdio --serve and --mcp are unchanged (private to their parent / the
  GUI's own token).

GHSA-c27p-xfx2-9r5x

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-30 21:48:20 +03:00
1 parent d0bfa48037
commit 1fec34d84a
7 files changed
+195 -60

No files matched your search

+7 -1
View File
@@ -14,6 +14,8 @@ sets, and a close-with-discard. Uses only the Python standard library.
"""
import json
import os
import secrets
import subprocess
import sys
import tempfile
@@ -24,10 +26,13 @@ import urllib.request
from pathlib import Path
TOKEN = secrets.token_hex(32)
def call(port: int, method: str, path: str, body=None):
"""One HTTP call; returns (status, parsed-json-or-None)."""
data = None
headers = {"Content-Type": "application/json"}
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"}
if body is not None:
data = json.dumps(body).encode()
request = urllib.request.Request(
@@ -67,6 +72,7 @@ def main() -> None:
out_dir = Path(tempfile.mkdtemp(prefix="ocs_rest_smoke_"))
process = subprocess.Popen(
[str(exe), "--http", str(port)],
env={**os.environ, "OCS_API_TOKEN": TOKEN},
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)