Skip to content

fix(server): return empty list + CORS header for untrusted /permission/pending#3880

Merged
sanity merged 1 commit intomainfrom
fix-permission-pending-cors
Apr 15, 2026
Merged

fix(server): return empty list + CORS header for untrusted /permission/pending#3880
sanity merged 1 commit intomainfrom
fix-permission-pending-cors

Conversation

@sanity
Copy link
Copy Markdown
Collaborator

@sanity sanity commented Apr 15, 2026

Problem

Lukas Orsvärn reported (Matrix, 2026-04-14) a console error when opening a contract-hosted image viewer:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at http://127.0.0.1:7509/permission/pending.
(Reason: CORS header 'Access-Control-Allow-Origin' missing).
Status code: 403.

/permission/pending was replying 403 Forbidden to any untrusted Origin header (e.g. null from a sandboxed iframe), but the 403 carried no Access-Control-Allow-* header, so the browser surfaced a "CORS header missing" error in devtools for every non-same-origin caller. The underlying behaviour (hiding live prompt contents from untrusted callers) was intentional, but the console noise looked like a real bug and made the contract-hosted webapp URL unusable without errors.

Solution

Always reply 200 OK with Access-Control-Allow-Origin: *, and return an empty [] body when the Origin is untrusted. The shell's polling loop silently ignores an empty list:

  • Trusted loopback origin → full prompt list (unchanged).
  • Missing Origin → full prompt list (unchanged — documented threat model).
  • Untrusted / null / cross-origin → empty [] with CORS header → browser can deliver the body, no console error.

Security posture is unchanged: a cross-origin or DNS-rebinding attacker still cannot learn live prompt contents, and /permission/{nonce}/respond retains its strict Origin check independently. * is safe on this endpoint because no credentials (cookies, auth tokens) are associated with the poll.

Testing

Three regression tests in permission_prompts::tests, all asserting both body shape and presence of the CORS header:

  • test_pending_prompts_untrusted_origin_returns_empty_with_cors — replaces the old test_pending_prompts_rejects_untrusted_origin and documents the new contract.
  • test_pending_prompts_null_origin_returns_empty_with_cors — regression for the reported bug (Origin: null from sandboxed iframes).
  • test_pending_prompts_trusted_origin_returns_list_with_cors — confirms trusted origins still see the real list and get the CORS header.

cargo test -p freenet --lib permission_prompts → 33 tests, all pass. cargo fmt --check clean. cargo clippy -p freenet --lib -- -D warnings clean.

Fixes

Addresses Lukas's Matrix report from 2026-04-14 (no issue number yet).

[AI-assisted - Claude]

…n/pending

The previous implementation replied `403 Forbidden` to any untrusted
`Origin` on `/permission/pending`, with no `Access-Control-Allow-*`
header. That caused the browser to surface a "CORS header missing"
error in the devtools console for every non-same-origin fetch
(e.g. from a sandboxed iframe whose origin is `null`), even though
the underlying behaviour — hiding live prompt contents from
untrusted callers — was intentional. The console error looked like a
real bug to users (reported on Matrix by Lukas Orsvärn, 2026-04-14)
and made the viewer URL for contract-hosted webapps spew noise.

Instead, always reply `200 OK` with
`Access-Control-Allow-Origin: *` and return an empty `[]` body when
the `Origin` is untrusted. The shell's polling loop silently ignores
an empty list, so legitimate same-origin callers still receive the
real prompt list and untrusted / null / cross-origin callers get a
valid-shape response they can read but which contains no data.

Security posture is unchanged: a cross-origin or DNS-rebinding
attacker still cannot learn prompt contents, and
`/permission/{nonce}/respond` retains its strict `Origin` check
independently. `*` is safe on this endpoint because no credentials
(cookies, auth tokens) are associated with the poll.

Regression tests cover three cases — untrusted origin, `null`
origin, and trusted origin — asserting both body shape and that the
CORS header is present on every branch. The previous
`test_pending_prompts_rejects_untrusted_origin` is replaced by
`test_pending_prompts_untrusted_origin_returns_empty_with_cors`,
which documents the new contract.

[AI-assisted - Claude]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Rule Review: No issues found

Rules checked: code-style.md, testing.md, git-workflow.md
Files reviewed: 1 (crates/core/src/server/client_api/permission_prompts.rs)

No rule violations detected.

Summary of what was verified:

  • No naked .unwrap() in production code — the new pending_prompts handler uses .unwrap_or(false) (fallible header decode → defaults to untrusted), which is correct.
  • fix: PR includes regression tests — three new test functions cover the specific bug scenarios:
    • test_pending_prompts_untrusted_origin_returns_empty_with_cors (evil.com → 200 + [] + CORS)
    • test_pending_prompts_null_origin_returns_empty_with_cors (sandboxed iframe Origin: null → 200 + [] + CORS)
    • test_pending_prompts_trusted_origin_returns_list_with_cors (loopback → 200 + full list + CORS)
  • Existing test updated, not deletedtest_pending_prompts_allows_missing_origin is enhanced (adds assertion on list length) rather than dropped.
  • No fire-and-forget spawns, no retry loops, no biased; selects introduced.
  • Code comment explains WHY, not WHAT — the large block comment on pending_prompts documents the security rationale and why 200 + [] is safe.

Rule review against .claude/rules/. WARNING findings block merge.

@sanity sanity merged commit d4947b9 into main Apr 15, 2026
12 checks passed
@sanity sanity deleted the fix-permission-pending-cors branch April 15, 2026 13:49
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.

1 participant