Public: anyone can read this; the crew can change it.

Every HTML, JSON and plain-text answer carries Cross-Origin-Resource-Policy: same-origin, and public assets and feeds say cross-origin on purpose #10975

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Every answer the forge writes now carries `Cross-Origin-Resource-Policy: same-origin`. Another site can no longer load a page, an API answer or an error into itself as a no-cors subresource (images, scripts and the like). A short public set says `cross-origin` on purpose, so embeds, feed readers and link previews keep working.

shipped production is serving it deploy 114667f

opened this

Every answer the forge writes now carries Cross-Origin-Resource-Policy: same-origin. Another site can no longer load a page, an API answer or an error into itself as a no-cors subresource (images, scripts and the like). A short public set says cross-origin on purpose, so embeds, feed readers and link previews keep working.

What changed

  • The floor: securityFloor in src/http/respond.ts sets cross-origin-resource-policy: same-origin whenever the response doesn't already have one.
  • The public set: a new module, src/http/corp.ts, lists the public paths, and the server marks their answers cross-origin just before the floor runs. I did this in one place, where every answer passes (answered in src/http/server.ts), rather than in each asset and feed handler, so a new handler can't forget it. The set is:

- Always cross-origin, because the bytes are the same for everyone: /assets/, /icons/, /favicon.ico, the apple-touch icons, /robots.txt, /llms.txt, /sitemap.xml, /manifest.webmanifest and /.well-known/. - Cross-origin only when the request carried no cookie or token: feeds (.atom) and share cards (/og/*). When asked for with a session they can show private content, so they stay same-origin. Feed readers, link previews and cross-site embeds send no session (the session cookie is SameSite=Lax), so nothing they use breaks. - Any error (status 400 or above) on a public path stays same-origin.

  • Not touched: answers that apps send back through the proxy don't pass through the floor, so their own headers are left as they are.

Tests: the new test/corp.test.ts fails without the change (2 of its 3 tests fail). It checks:

  • Pages, API answers (signed in and out) and 404s are same-origin, as are a feed and a share card asked for with a session.
  • Assets, the changelog and repository feeds, the crawler and agent files, the manifest, /.well-known/security.txt, the favicon and the share cards (site svg and png, repository svg) are cross-origin, as the issue's risk note asked.
  • A unit check of the path rules.

docs/SECURITY-RUNBOOK.md has a new section that names the header, explains why the public set is exempt, and says how to check a broken link preview with curl.

Checks run: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named 1839 test files, too many to run here, so I chose these myself and they pass: corp, both security-floor suites, security-controls, web, webspec, scaffold-webspec, atom-feeds, feeds, cache-private, caching-doc, compress-serve, og-private, og-route and llms-anonymous. The forge's full check runs on the push.

Try it: run curl -sI <preview>/og/site.png and curl -sI <preview>/api/v1/user. The first should answer cross-origin, the second same-origin.

🤖 Generated with Claude Code

Closes #10965

commented

Every answer the forge writes now carries Cross-Origin-Resource-Policy: same-origin. Another site can no longer load a page, an API answer or an error into itself as a no-cors subresource (images, scripts and the like). A short public set says cross-origin on purpose, so embeds, feed readers and link previews keep working.

What changed

  • The floor: securityFloor in src/http/respond.ts sets cross-origin-resource-policy: same-origin whenever the response doesn't already have one.
  • The public set: a new module, src/http/corp.ts, lists the public paths, and the server marks their answers cross-origin just before the floor runs. I did this in one place, where every answer passes (answered in src/http/server.ts), rather than in each asset and feed handler, so a new handler can't forget it. The set is:

- Always cross-origin, because the bytes are the same for everyone: /assets/, /icons/, /favicon.ico, the apple-touch icons, /robots.txt, /llms.txt, /sitemap.xml, /manifest.webmanifest and /.well-known/. - Cross-origin only when the request carried no cookie or token: feeds (.atom) and share cards (/og/*). When asked for with a session they can show private content, so they stay same-origin. Feed readers, link previews and cross-site embeds send no session (the session cookie is SameSite=Lax), so nothing they use breaks. - Any error (status 400 or above) on a public path stays same-origin.

  • Not touched: answers that apps send back through the proxy don't pass through the floor, so their own headers are left as they are.

Tests: the new test/corp.test.ts fails without the change (2 of its 3 tests fail). It checks:

  • Pages, API answers (signed in and out) and 404s are same-origin, as are a feed and a share card asked for with a session.
  • Assets, the changelog and repository feeds, the crawler and agent files, the manifest, /.well-known/security.txt, the favicon and the share cards (site svg and png, repository svg) are cross-origin, as the issue's risk note asked.
  • A unit check of the path rules.

docs/SECURITY-RUNBOOK.md has a new section that names the header, explains why the public set is exempt, and says how to check a broken link preview with curl.

Checks run: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named 1839 test files, too many to run here, so I chose these myself and they pass: corp, both security-floor suites, security-controls, web, webspec, scaffold-webspec, atom-feeds, feeds, cache-private, caching-doc, compress-serve, og-private, og-route and llms-anonymous. The forge's full check runs on the push.

Try it: run curl -sI <preview>/og/site.png and curl -sI <preview>/api/v1/user. The first should answer cross-origin, the second same-origin.

🤖 Generated with Claude Code

commented

Every answer the forge writes now carries Cross-Origin-Resource-Policy: same-origin, so another site can no longer load a page, an API answer or an error into itself as a no-cors subresource. A short list of public paths says cross-origin on purpose, so embeds, feed readers and link previews keep working.

What changed

  • The floor: securityFloor in src/http/respond.ts sets cross-origin-resource-policy: same-origin whenever a response has none.
  • The public set: a new module, src/http/corp.ts, lists the public paths, and answered in src/http/server.ts marks their successful answers cross-origin before the floor runs. It is done in that one place, where every answer passes, so a new asset or feed handler can't forget it.

- Always cross-origin, because the bytes are the same for everyone: /assets/, /icons/, /favicon.ico, the apple-touch icons, /robots.txt, /llms.txt, /sitemap.xml, /manifest.webmanifest and /.well-known/. - cross-origin only when the request carried no cookie or token: feeds (.atom) and share cards (/og/*, svg and png). Asked for with a session, they can hold private content, so they stay same-origin. Feed readers, link previews and cross-site embeds send no session, because the cookie is SameSite=Lax. - An error (status 400 or above) on a public path stays same-origin.

  • Apps: what an app sends back through the proxy doesn't pass through the floor, so it keeps its own headers.
  • Runbook: docs/SECURITY-RUNBOOK.md has a new section that names the header, explains why the public set is exempt, and shows how to check a broken link preview with curl.

Tests: the new test/corp.test.ts fails without the change (two of its three tests fail). It checks:

  • Pages, API answers (signed in and out) and 404s are same-origin, and so are a feed and a share card asked for with a session.
  • Assets, the changelog and repository feeds, the crawler and agent files, the manifest, /.well-known/security.txt, the favicon and the share cards are cross-origin, as the issue's risk note asked.
  • The path rules themselves, as a unit check.

The first check failure: mcp-write-scope and mcp-system-stalls failed on this pull, but they also fail at the commit the branch started from, and they pass on current main. I merged main into the branch, and both now pass along with the CORP and security-floor tests. bun scripts/gate.ts and bun x tsc --noEmit pass.

Try it: curl -sI <preview>/og/site.png should say cross-origin, and curl -sI <preview>/api/v1/user should say same-origin.

🤖 Generated with Claude Code

Approved

Clean. The floor sets same-origin by default and the public set is marked once, where every answer passes. Feeds and share cards go cross-origin only with no cookie or token, and errors stay same-origin. No findings. Tests cover page, API, 404, asset and feed. The runbook section is present. The preview answers cross-origin on /og/site.png.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs