Skip to content

[P2 security] No CSRF token on require_local writes, including the /promote airlock #22

Description

@marsrobertson

Summary

Local-only writes have no CSRF token. Locality is inferred from the absence of a header that browsers never set cross-origin, so a page in the operator's browser can POST JSON to these routes using a simple-request content type, and the request is indistinguishable from a deliberate operator action.

Evidence

Unprotected local-only writes:

  • fleet/bin/fleet.py:1086/api/signatures/judge, rewrites the whole signatures file (fleet.py:1112).
  • fleet/bin/fleet.py:1057/api/convene, subprocess.Popen at fleet.py:1074.
  • Every require_local cockpit write: approvals approve/revoke (main.py:556, 595), handoffs (main.py:655), touch (main.py:672), horizons (main.py:718), triage (main.py:1130), promote (main.py:1211).

Already protected, and the model to follow: /api/kill (fleet.py:1151) and /api/paste-image (fleet.py:1132) require KILL_TOKEN in the body. That is what the token is actually good for — /api/kill-token (fleet.py:868) hands it to any same-origin page, so it is a CSRF token, not an authN credential.

Impact

Includes /api/signals/{id}/promote, which is the human airlock crossing from inbox.json into life.json. A CSRF against it means a stranger's signal is promoted without the operator ever reading it.

Composes with the stored-XSS issue: same-origin XSS reads the token, so a CSRF token alone is insufficient while that is open. Both need fixing.

Acceptance criteria

  • Local-only writes require KILL_TOKEN, matching /api/kill.
  • Tests cover a cross-origin simple-request POST to at least /api/convene and /api/signals/{id}/promote.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions