Skip to content

fix(sw): pin CSP/initiator handlers to sender origin (RQ-3050) - #98

Open
dinex-dev wants to merge 1 commit into
masterfrom
security/rq-3050-pin-initiator-to-sender-origin
Open

fix(sw): pin CSP/initiator handlers to sender origin (RQ-3050)#98
dinex-dev wants to merge 1 commit into
masterfrom
security/rq-3050-pin-initiator-to-sender-origin

Conversation

@dinex-dev

Copy link
Copy Markdown
Member

Summary

Two message handlers in the MV3 service worker derived a target origin from the caller-supplied requestDetails.initiator. That message is relayed from the page's MAIN world over the forgeable source: "requestly:client" channel, so any web page could set initiator to an arbitrary origin. Both are now pinned to the browser-provided sender origin, which a page cannot forge.

What was vulnerable

  • onErrorOccurredhandleCSPError (RQ-3050): removes a site's Content-Security-Policy response header for requestDetails.initiator. A forged initiator let a page arm a tab-scoped rule that strips a third-party origin's CSP, weakening its XSS/clickjacking defenses.
  • onBeforeAjaxRequesthandleInitiatorDomainFunction (sibling, found in review): stamps requestDetails.initiator into headers for rules using rq_request_initiator_origin(). A forged initiator let a page spoof the origin value that feature is meant to certify (conditional on the victim having a matching enabled Headers rule). Lower severity — no CSP removal, rule-gated, tab-scoped — but the same root cause.

The fix

  • New shared helper getTrustedSenderOrigin(sender) derives the origin from sender.origin (falling back to a guarded new URL(sender.url).origin) — browser-populated, unforgeable — returning undefined/"null" for indeterminate/opaque frames.
  • onErrorOccurred: pins the CSP target to the sender origin; skips when there is no trusted origin (fail-closed).
  • onBeforeAjaxRequest: overwrites initiator with the sender origin before processing. url / requestHeaders remain caller-supplied (the AJAX target is legitimately cross-origin).

Why this is safe (no behavior change for real traffic)

The legitimate emitter (ajaxRequestInterceptor) always sends initiator === location.origin and posts same-frame, so sender.origin equals the value it already sends — the pin is a no-op for genuine traffic and only discards forged cross-origin values. A per-frame comment documents the "same-frame emitter" invariant.

Testing

  • Built (npm run build:current, clean) and loaded unpacked; both pins verified in dist/serviceWorker.js.
  • CSP (RQ-3050): Insert-Script rule still injects on a strict-CSP site (github.com) and produces a CSP-removal rule for its own origin; a forged onErrorOccurred with a cross-origin initiator no longer creates a rule for that origin.
  • Initiator header: rq_request_initiator_origin() still stamps the true page origin (incl. iframe / cross-origin XHR variants); a forged initiator is overridden with the real sender origin.

Notes / follow-ups (not in this PR)

  • RQ-3058 (cacheSharedState, same file) is a related, still-open issue — will be handled separately after its own review.
  • The requestly-automation package vendors a built copy of this extension; it will be re-vendored after this PR merges.
  • Branch was cut from a master that is a few commits behind origin/master; rebase before merge if needed.

🤖 Generated with Claude Code

The onErrorOccurred and onBeforeAjaxRequest message handlers derived the
target origin from the caller-supplied requestDetails.initiator. That message
is relayed from the page MAIN world over the forgeable "requestly:client"
channel, so any web page could set initiator to an arbitrary origin.

- onErrorOccurred -> handleCSPError removes a site's Content-Security-Policy
  header for requestDetails.initiator (RQ-3050). A forged initiator let a page
  strip a third-party origin's CSP (tab-scoped).
- onBeforeAjaxRequest -> handleInitiatorDomainFunction stamps
  requestDetails.initiator into headers for rules using
  rq_request_initiator_origin(). A forged initiator let a page spoof that
  origin value (conditional on a matching enabled rule).

Both now pin initiator to the browser-provided sender origin (sender.origin /
sender.url), which a page cannot forge, via a shared getTrustedSenderOrigin()
helper (URL parsing guarded). The legitimate emitter already sends
initiator === location.origin === sender origin, so this is a no-op for real
traffic; forged cross-origin values are discarded.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
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