Skip to content

Close the world server to cross-origin control; irreversible acts need the grant - #25

Merged
HarperZ9 merged 2 commits into
mainfrom
fix/security-20261001
Oct 1, 2026
Merged

HarperZ9 merged 2 commits into
mainfrom
fix/security-20261001

Conversation

@HarperZ9

@HarperZ9 HarperZ9 commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Problem

  1. The shared world server (python -m accountable_surface.world.server, port 8808) sent Access-Control-Allow-Origin: * on every response and checked no token, Host or Origin. Any web page the operator visited could POST /act, /autopilot and /upload. A text/plain POST skips the browser preflight, so it could write even without the wildcard. Reproduced on main (d5a7c10): a cross-site text/plain POST from Origin: http://evil.example returned 200 and wrote the file.
  2. allow_irreversible=True alone authorized an irreversible act. Any caller holding a grant for the action could pass the flag.

Change

World server (world/access.py, world/server.py, web/token.js):

  • Binds loopback only. A non-loopback host raises; ACCOUNTABLE_WORLD_HOST=0.0.0.0 no longer publishes it.
  • Generates a 32-byte token per run, prints it in the URL, and requires it on every API route (header X-World-Token; the event stream alone also takes ?token= because EventSource cannot set headers). Constant-time compare.
  • Refuses any request whose Host or Origin is not the server's own (stops DNS rebinding), refuses cross-site Sec-Fetch-Site, refuses non-JSON POSTs.
  • Sends no CORS headers. OPTIONS always refuses.
  • The web UI keeps the token in the tab (sessionStorage) and strips it from the address bar.
  • Clamps a negative Content-Length (it hung the handler), drains small bodies before refusing (Windows otherwise resets the socket and the client loses the refusal), and refuses the world root itself as a write target (an empty target crashed the handler).

Irreversible actuation (grant.py, surface.py):

  • The grant must list the action kind in scope.allowed_irreversible_actions. Only a list of exact kind strings counts: no wildcard, boolean or string (a string would turn membership into a substring test).
  • allow_irreversible=True stays as a second opt-in that can only narrow. The MCP path still passes False, so remote irreversible acts stay needs-human.
  • The new field is a local scope field and is stripped before proof-surface's closed schema.
  • Breaking for library callers that relied on the flag alone. Tests that pre-authorize an irreversible act now say so in the grant (tests/_irreversible.py).

Tests

  • New: tests/test_world_access.py (29) and tests/test_irreversible_grant.py (12). Against main they were 0 passed (17 failed, 24 errors).
  • Full suite, Windows (Python 3.12, sibling coherence-membrane 53177af and proof-surface 553cbac): 557 passed, 1 skipped. Run three times; test_world_access.py looped 10 more times, all green.
  • Full suite, WSL Ubuntu native clone (Python 3.12.3): 557 passed, 1 skipped.
  • Node web tests: 5 passed.
  • Browser smoke test on a live server: the printed URL loads, the token leaves the address bar, Propose & act writes and verifies the file, /watch streams, and a tokenless /world returns 401.

Limits

Grants are unsigned dicts, so the grant is authority only as far as the code that builds it is the operator's. On the MCP path the authority store builds it and the flag stays False. Not a version release.

🤖 Generated with Claude Code

HarperZ9 and others added 2 commits October 1, 2026 12:10
…d the grant

The shared world server sent Access-Control-Allow-Origin: * on every response
and checked no credential, Host or Origin, so any page the operator visited
could POST /act, /autopilot and /upload to 127.0.0.1:8808. A text/plain POST
skips the browser preflight, so the wildcard was not even needed to write.

- Bind loopback only; a non-loopback host raises instead of warning.
- Generate a token per run, print it in the URL, and require it on every API
  route (X-World-Token; the event stream alone also takes ?token=).
- Refuse requests whose Host or Origin is not the server's own, which also
  stops DNS rebinding; refuse cross-site Sec-Fetch-Site and non-JSON POSTs.
- Send no CORS headers; OPTIONS always refuses.
- The web UI keeps the token in the tab and strips it from the address bar.
- Clamp a negative Content-Length, drain small bodies before refusing, and
  refuse the world root itself as a write target (it crashed the handler).

Irreversible actuation now needs scope.allowed_irreversible_actions in the
grant to name the action kind. allow_irreversible=True stays as a second
opt-in that can only narrow; alone it authorizes nothing. No wildcard,
boolean or string value counts. Tests that pre-authorize an irreversible act
now say so in the grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HarperZ9
HarperZ9 merged commit 252b99b into main Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant