fix(auth): scope embedded key to parent origin + validate MessageChannel sender (ENG-4596) - #131
Open
justinformentin wants to merge 1 commit into
Open
Conversation
…nel sender (ENG-4596)
Fixes two cross-origin vulnerabilities in the auth frame (ENG-4596 / TKA-20260806-016):
## Vuln A — MessageChannel gate accepts any sender
Before this fix the TURNKEY_INIT_MESSAGE_CHANNEL handler gated only on
event.ports?.[0], allowing any window (not just the direct parent) to
establish the privileged MessageChannel.
Fix: mirror the INT-697 export-and-sign pattern. The gate now requires:
- event.source === window.parent
- event.origin is truthy and !== 'null'
- event.ports?.length === 1
## Vuln B — embedded key not scoped to parent origin
Before this fix a single P-256 private key was persisted in localStorage
under the fixed name 'TURNKEY_EMBEDDED_KEY', created before any parent
origin was known. The same key (and PUBLIC_KEY_READY public key) was
served to every embedder, enabling an attacker to replay an auth bundle
from one origin into the same key on another origin.
Fix (mirrors INT-697 export-and-sign approach, applied inline in auth/index.html):
- validateParentOrigin() validates the origin is non-opaque
- initEmbeddedKey(origin) stores the key under a V2 scoped storage key
'TURNKEY_EMBEDDED_KEY_V2:<encodeURIComponent(origin)>'
- initEphemeralEmbeddedKey() creates a memory-only key for legacy clients
- purgeLegacyEmbeddedKey() removes the old fixed key on every init
- embeddedKeyState tracks mode ('persistent' or 'ephemeral') + origin/key
- getBoundOrigin() exposes the current bound origin for re-init
- Standalone mode (window.parent === window): persists key scoped to
window.location.origin
- Embedded mode on DOMContentLoaded: creates ephemeral key for legacy
(@turnkey/iframe-stamper < 2.1.0) clients
- MessageChannel handshake: supersedes ephemeral with persistent key
scoped to event.origin; rolls back channelEstablished on failure
- Legacy embedded path: binds to first sender's origin; rejects others
Linear: ENG-4596 — https://linear.app/turnkey/issue/ENG-4596
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR ports the INT-697 cross-origin hardening pattern to the auth frame, closing two related vulnerabilities (ENG-4596 / TKA-20260806-016, Medium).
The auth frame is structurally different from export-and-sign: the source is entirely inline in
auth/index.html(nosrc/directory, no build step). This PR adapts the same security patterns faithfully to the inline-script architecture.Vuln A — MessageChannel gate accepts any sender
Before: The
TURNKEY_INIT_MESSAGE_CHANNELhandler gated only onevent.ports?.[0], allowing any window (not just the direct parent) to establish the privileged MessageChannel and receive subsequent messages via the port.After: The gate now validates source, origin, and port count — mirroring the INT-697 export-and-sign fix:
Vuln B — Parent-agnostic embedded key
Before: A single P-256 private key was persisted in localStorage under the fixed name
"TURNKEY_EMBEDDED_KEY", created at DOMContentLoaded before any parent origin was known. The same key (and thus the samePUBLIC_KEY_READYpublic key) was served to every embedder. An attacker could replay an auth bundle encrypted to that key from any origin.After: The inline TKHQ IIFE now tracks an
embeddedKeyStateobject with two modes:persistent— key stored in localStorage underTURNKEY_EMBEDDED_KEY_V2:<encodeURIComponent(origin)>. Each parent origin gets its own isolated key slot.ephemeral— key stored in memory only (for legacy@turnkey/iframe-stamper < 2.1.0clients). Unique to this document; never persisted.Key new helpers added inline:
validateParentOrigin(origin)— validates non-empty, non-opaque, valid serialized originpurgeLegacyEmbeddedKey()— removes oldTURNKEY_EMBEDDED_KEY(never migrated)initEmbeddedKey(origin)— now REQUIRES an origin; creates/reuses V2 scoped persistent keyinitEphemeralEmbeddedKey()— creates in-memory-only key; no-ops if persistent key is activegetBoundOrigin()— returns the currently bound origingetEmbeddedKey(),setEmbeddedKey(),resetEmbeddedKey()to dispatch based on state modeFlow changes:
initEmbeddedKey(window.location.origin)initEphemeralEmbeddedKey()for legacy clientsinitEmbeddedKey(event.origin)supersedes ephemeral; rolls back on failureTests
Added/updated in
auth/index.test.js(34 total, all passing):New Vuln B tests: different origins produce different keys, refuses second origin binding, rejects invalid origins, purges legacy key, ephemeral key stays in memory only, getBoundOrigin coverage.
New Vuln A tests (channel gate): rejects non-parent source, empty origin, opaque
"null"origin, 0 ports, 2 ports; accepts valid event.Validation Results
No build step exists for the auth frame (inline HTML is the source). No dist artifacts to commit.
References