Repository navigation
Refuse a state-changing request another page sent - #274
Merged
Merged
Conversation
adityak74
force-pushed
the
235-cross-site-guard
branch
from
October 4, 2026 02:06
0d475da to
8efeffa
Compare
This was referenced Oct 4, 2026
CORS stops a foreign page from reading this server's answers. It does not stop the browser from sending a simple POST, and the loopback install has no token, so a handler that takes no JSON body ran for any page the person visited. auth::refuse_cross_site runs on every route, inside the CORS layer. A non-GET request passes when: - Sec-Fetch-Site is same-origin or none - for a browser that does not send that header, Origin matches Host - Origin is named with --allow-origin - it carries neither header (curl, the CLI, the Mac app) Anything else gets 403 before a handler runs. Another loopback port reads same-site and is refused. This is the first slice of #235: a preview frame runs a dev server's script one port away from this page, and the guard has to exist first. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adityak74
force-pushed
the
235-cross-site-guard
branch
from
October 4, 2026 03:04
8efeffa to
b2a2a5a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #235. This is the first slice, the prerequisite named in the design proposal on that issue. It does not close #235.
Problem
The CORS layer (DECISIONS 2026-08-20) stops a foreign page from reading zorp-web's answers. It does not stop a browser from sending a "simple" cross-site request: a POST with a
text/plainbody or no body at all. The default loopback install has no token, so any handler that takes no JSON body ran for whatever page the person had open. Handlers that take JSON were protected only because axum'sJsonextractor rejectstext/plain, and nothing states that as a rule.#235 is about to put a dev server, which is model-written JavaScript, in a frame one port away from this page. So this has to exist first.
Change
auth::refuse_cross_site, a middleware on every route, just inside the CORS layer:Sec-Fetch-Siteissame-originornoneSec-Fetch-Site(older browsers) andOriginmatchesHostOriginis named with--allow-originDesign choices
Sec-Fetch-Siterather thanOriginvsHost. The compose UI's nginx (web/nginx.conf.template) proxies/api/withHostrewritten to the upstream, and attaches the bearer token for anyone who reaches it. So in the container split, neitherHostnor the token can distinguish the UI from another page. A page cannot forgeSec-Fetch-Site, and the UI's own requests readsame-originthrough the proxy.127.0.0.1:5173readssame-site, because a site ignores ports. That is exactly the dev-server case Render a running web app in the side pane, not just the files it wrote #235 adds, and it is refused.docs/DECISIONS.mdentry (2026-10-03).Tests:
zorp-web/tests/cross_site.rs, 7 tests over real HTTPa_page_on_another_site_cannot_create_anythingPOST /api/sessions(bodyless, always 200 when it runs) from a cross-site page gets 403a_page_on_another_loopback_port_is_refusedsame-sitefrom127.0.0.1:5173gets 403trusting_an_agent_from_another_page_never_reaches_the_handlerthe_servers_own_page_is_allowedsame-origingets 200, even with a differentOriginandHost(the nginx case)a_request_that_is_not_from_a_page_is_allowedwithout_fetch_metadata_the_origin_must_match_the_hostOrigingets 200; foreign gets 403;nullgets 403a_named_origin_is_allowed--allow-originorigin gets 200Validation
All cargo commands were run one at a time with
CARGO_BUILD_JOBS=4, debug info off and incremental off, each under a 30-minutealarm.cargo test -p zorp-web --test cross_site: 7 passed.cargo test -p zorp-web --no-fail-fast: 290 passed, 0 failed. No existing test was affected.cargo fmt --all -- --check: passed.cargo clippy -p zorp-web --all-targets --locked -- -D warnings: passed. CI runs this workspace-wide, but this diff touches onlyzorp-web.Not run, and why:
cargo test --workspace. The change is confined tozorp-web, and the run guidelines for this batch skip the full workspace suite unless the issue calls for it. CI will run it.web/tests. No TypeScript changed, and the browser sendsSec-Fetch-Siteon its own.the_servers_own_page_is_allowed, which sends a mismatchedHostandOriginwithsame-origin.Left for #235
Per the proposal on the issue:
POST /api/preview/check🤖 Generated with Claude Code