Skip to content

fix(web): same-origin check on cookie-authenticated mutations - #103

Merged
mrgoonie merged 14 commits into
mainfrom
claude/modest-wozniak-8uwf2a
Oct 20, 2026
Merged

mrgoonie merged 14 commits into
mainfrom
claude/modest-wozniak-8uwf2a

Conversation

@mrgoonie

@mrgoonie mrgoonie commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

SameSite=Lax still lets a sibling subdomain send the session cookie. These routes read their bodies with request.json(), which does not require application/json. A text/plain POST, which needs no preflight, could therefore act as the signed-in user.

  • favorite, vote, rate, review (action): these four authenticate with getSession only, so none of them has a Bearer path. crossOriginRefusal runs before the session is read. A refused request gets 403.
  • register: authenticateRequest can fall back to the session. The check runs only when method === "session", so CLI publish and use calls that send a key are unaffected.
  • resolveLimitedActor callers: these are release state, promote, review, trusted publishers, collections writes, personal-kit writes, and publisher provisioning. When neither Authorization nor X-Admin-Secret is present and the origin is foreign, the request gets 403 cross_origin. That happens before any limiter runs, any session is read, or the DB is touched. A key or the admin secret is never sent automatically by the browser, and both force a CORS preflight anyway.
  • Refactor: the capped JSON body reader moves to release-json-body.ts and is re-exported, so existing callers keep their imports. This keeps release-state-http.ts under the 200-line limit.
  • Docs (docs(api): vote endpoint documented with the wrong request/response shape #102):
    • The vote API is documented with its real shape: {type: up|down|none} in, {vote_type, net_votes} out, and 429.
    • The vote entry is added to the in-app API docs.
    • Also fixed: /api/report auth (API key only), the votes.vote_type column name, the protected-endpoint table, and the stale vote flow.
    • The nonexistent /api/skills/:slug/references route is dropped from the docs.
    • cross_origin is documented for release state, publishers, collections, promote and personal kits.
    • CLAUDE.md gets a same-origin caveat and an issue-claim label workflow.

Tests

  • New app/routes/api.cookie-mutation-origin.test.ts, with one block per route:
    • A foreign Origin, a sibling subdomain, http: on the same host, and Sec-Fetch-Site: same-site/cross-site each get 403, with no session read and no write.
    • Same-origin, Sec-Fetch-Site: same-origin/none, and header-less requests get 200.
    • For register, an API key with a foreign Origin still passes.
  • New tests/releases/release-state-origin.test.ts and a new case in tests/personal-kits/kit-revisions.test.ts:
    • A cross-origin session gets 403 cross_origin and nothing is written. The audit is empty, and the follow-up write with expectedGeneration: 0 gets 201.
    • Key and admin requests are unaffected.
  • With the fixes reverted, the new refusal tests fail. With the fixes in place, they pass.
  • After merging main (fix: alias slugs, .pi untracking, release scan tuning, API key settings follow-ups #96, feat: collections API and verified CLI installer with lockfile, sync and local MCP (#35 phase 3) #98, fix(scan): normalize separators before prompt-injection matching #111), pnpm typecheck is clean and npx vitest run passes 115 files (2309 passed, 2 skipped). pnpm build succeeds.
  • I could not run the local dev server, because miniflare fails to start behind this environment's proxy. Instead, the tests call the route handlers with real Request headers.
  • An isolated code-review subagent reviewed the change twice. It raised no blocking findings, and all medium and low items were addressed.

Closes #101
Closes #102

🤖 Generated with Claude Code

https://claude.ai/code/session_01S4FiHPufhLdcCyhMszKa95

mrgoonie and others added 9 commits October 19, 2026 19:24
… usage routes

Closes #47

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Closes #49

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…efore scan

Closes #61

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Closes #79

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ndow edges

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SameSite=Lax still lets a sibling subdomain send the session cookie, and
these routes parse bodies without requiring application/json, so a
no-preflight POST could act as the signed-in user.

- favorite, vote, rate and review (session-only) call crossOriginRefusal
  before reading the session.
- register checks only when authenticateRequest fell back to the session.
- resolveLimitedActor (release state, personal kits, publisher provision)
  answers 403 cross_origin when neither Authorization nor X-Admin-Secret
  is present and the origin is foreign.

API-key and admin-secret requests are not ambient and stay unchecked.

Refs #101

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4FiHPufhLdcCyhMszKa95
The vote endpoint takes { type: up|down|none } and returns
{ vote_type, net_votes }; docs described { direction } with toggle-off.
Also fix /api/report auth (API key only), the votes column name, the
protected-endpoint table, and drop the nonexistent references route.
Document the same-origin rule and the issue claim labels in CLAUDE.md.

Refs #102

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4FiHPufhLdcCyhMszKa95
…e-http

release-state-http.ts was past the 200-line limit. The capped
application/json body reader moves to release-json-body.ts and is
re-exported, so callers keep their imports. The same-origin state tests
get their own file, and the publisher provisioning docs list the
403 cross_origin case.

Refs #101

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4FiHPufhLdcCyhMszKa95

Copy link
Copy Markdown
Contributor Author

Status: CI (web-contracts-cli) passed on 2be5285. I merged the latest #96 head (eff407f) in as f94ff46. Locally, typecheck is clean and 2119 tests pass.

Blocked on #96. This PR is stacked on sx-bugs-47-49-61-79, and #96 is still open and still receiving commits. Merging this PR into main first would also land all of #96 without its own review, so I am not merging it. Once #96 merges, I will merge main back in so this diff shrinks to its own three commits, then merge this PR.


Generated by Claude Code

mrgoonie and others added 5 commits October 19, 2026 20:46
…indow-edge false positives

A licence file that is not valid UTF-8 skipped the instruction scan entirely.
It is now decoded lossily and scanned; markup such as LICENSE.md never gets the
latin-1 allowance and is quarantined as non-UTF-8 text.

A secret token ending exactly at a non-final window edge was matched because
the end of the string acted as a word boundary; those matches are now left to
the next window, which sees the following bytes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ence names, keep long tokens at window edges

A non-UTF-8 licence file is now scanned as lossy UTF-8 and as latin-1 (soft
hyphens removed), so NBSP or soft-hyphen tricks that only a latin-1 reader
sees no longer slip past the instruction scan.

Only a match lying wholly inside the window overlap is treated as a cut
artifact; a long secret that began before the overlap is still reported.

Licence names are upper-case LICENSE/LICENCE/COPYING/NOTICE/AUTHORS with an
optional upper-case suffix and .txt/.md/.markdown; free suffixes such as
NOTICE-install or LICENSE.rst keep every rule.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nical names

Licence-like names in any spelling (NOTICE-install, LICENSE_MIT, PATENTS,
license) are instruction documents again, so injection, shell/URL syntax and
non-UTF-8 text are all checked. Only the canonical upper-case names get the
syntax and latin-1 exemptions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keeps the same-origin refusal in resolveLimitedActor alongside main's
narrowed return type, so promote, review and collections writes inherit it;
documents cross_origin on those endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4FiHPufhLdcCyhMszKa95
@mrgoonie
mrgoonie merged commit 10473ed into main Oct 20, 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

2 participants