Skip to content

fix: make IPC CORS headers agree with the request guard - #91

Open
ZK-Andy wants to merge 1 commit into
Yupmoh:mainfrom
ZK-Andy:fix/ipc-cors-loopback-origin
Open

fix: make IPC CORS headers agree with the request guard#91
ZK-Andy wants to merge 1 commit into
Yupmoh:mainfrom
ZK-Andy:fix/ipc-cors-loopback-origin

Conversation

@ZK-Andy

@ZK-Andy ZK-Andy commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #90.

Problem

The IPC request guard and the IPC CORS headers decided differently:

  • IsAuthorized accepts the configured cross-origin or any loopback origin;
  • BuildCorsHeaders echoed Access-Control-Allow-Origin only for the single allowedCorsOrigin string captured from RynOptions.Url when the window was created.

So when the page's loopback origin changes during a session (dev server restarted on another port, crash-restart port fallback), the request is authorized server-side but the browser drops it at the CORS preflight — the host never sees it and nothing is logged. HandleIpcEvalAsync had the same mismatch in a simpler form: it returned no CORS headers at all.

Change

  • Extract one IsAllowedIpcOrigin(origin) predicate (configured origin ∪ any loopback origin) and use it from both IsAuthorized and BuildCorsHeaders, so the two can no longer drift apart.
  • /ipc/eval/ responses now carry the same CORS headers (503/403/400/200 paths) instead of none.
  • Emit Vary: Origin whenever the request carries an Origin — the allow-origin decision is origin-dependent either way, so responses must not be cached across origins.

Security note: this does not widen the boundary. The per-launch X-Ryn-Token remains the authorization gate (constant-time compared); CORS only lets the browser deliver a response for a request the server already accepted. A cross-site caller still cannot read anything without the token, and non-loopback origins are still rejected by IsAuthorized and receive no Access-Control-Allow-Origin.

Tests

New tests/Ryn.Core.Tests/LocalWebServerCorsTests.cs (raw socket, matching the existing LocalWebServerRawSocketTests style) asserts, against LocalWebServer(contentDirectory: null, allowedCorsOrigin: "http://127.0.0.1:31000"):

  • preflight from the configured origin and from a different loopback origin both carry Access-Control-Allow-Origin + Vary: Origin;
  • preflight and command from a non-loopback origin carry no Access-Control-Allow-Origin, and the command is still 403;
  • an authorized /ipc/cmd/ and /ipc/eval/ response from the second loopback origin carries Access-Control-Allow-Origin.

Local result: Ryn.Core.Tests → 202 passed, 2 skipped, 5 failed. The 5 failures are all WindowStatePersistenceTests, which persist under LocalApplicationData; that path is a read-only filesystem in my sandbox (mkdir: read-only file system), so Save silently fails and Load returns null. They are unrelated to this change and fail the same way on a pristine checkout here; the new CORS tests pass 5/5 (plus the eval case).

Notes for review

  • The predicate change means a loopback origin other than the configured one now also gets Access-Control-Allow-Origin in static/local-server mode (previously no CORS headers were emitted at all when allowedCorsOrigin was null). That is the same consistency fix: authorization already accepted such origins there. Same-origin pages send no Origin, so their responses are unchanged.
  • I kept the fix to one predicate + one header path rather than adding an "update the allowed origin on navigation" API; if you would rather expose the latter, I am happy to rework it.

- share one IsAllowedIpcOrigin predicate between IsAuthorized and
  BuildCorsHeaders, so the response can no longer deny an origin the request
  guard accepted (a page that moved to a different loopback port mid-session
  was authorized but got no Access-Control-Allow-Origin, and the browser
  dropped every window.__ryn.invoke)
- send the same CORS headers on /ipc/eval/ responses, which returned none and
  left a CORS error in DevTools for a request the server had accepted
- emit Vary: Origin whenever the request carries an Origin, since the
  allow-origin decision is origin-dependent either way
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.

IPC CORS allow-origin is pinned to the launch URL: a page that changes loopback port is authorized but CORS-blocked

1 participant