fix(import): extend cross-origin MessageChannel validation to import frame [INT-783][ENG-4597][ENG-4598] - #130
Conversation
… import frame Mirrors the TURNKEY_INIT_MESSAGE_CHANNEL gate added to export-and-sign in PR #129 (#129). The import frame's handler previously only checked event.ports?.[0]; it now also validates: - event.source === window.parent (direct parent only) - event.origin && event.origin !== "null" (concrete, non-opaque origin) - event.ports?.length === 1 (exactly one transferred port) Closes the identical class of vulnerability INT-697 fixed in export-and-sign. Pre-existing gap; not introduced by PR #129. Also adds six Jest tests for the new gate conditions and rebuilds the production bundle (content hash: 45de86a592cd0d80ccd0). Linear: INT-783 https://linear.app/turnkey/issue/INT-783
207de18 to
f1647ae
Compare
…ing (ENG-4597) Previously, onInjectImportBundle() in the v1.0.0 path would only log a console.warn when organizationId or userId was omitted by the caller, allowing signed import bundles to be accepted without org/user binding. This change converts both warnings into hard errors: - Missing organizationId → throws with message indicating iframe-stamper >= v2.0.0 is required - Missing userId → throws with same guidance The existing equality checks (signedData must match the supplied values) are preserved unchanged. The now-obsolete TODO comments and console.warn calls are removed. Tests: new describe block 'onInjectImportBundle org/user binding (import frame)' in import/index.test.js covers: - missing organizationId (undefined and empty string) - missing userId (undefined and empty string) - mismatched organizationId / userId still throws - valid matching bundle succeeds (BUNDLE_INJECTED) Bundle rebuilt: index.bundle.808edbe601034931b90e.js
1a7b31a to
e0a04e0
Compare
fainashalts
left a comment
There was a problem hiding this comment.
The MessageChannel gate itself looks correct. Left a blocking comment but otherwise looks good!
| event.data && | ||
| event.data["type"] == "TURNKEY_INIT_MESSAGE_CHANNEL" && | ||
| event.ports?.[0] | ||
| event.source === window.parent && |
There was a problem hiding this comment.
this validates only the MessageChannel handshake. The legacy listener registered in DOMContentLoaded still attaches messageEventListener directly, so until a channel is established INJECT_IMPORT_BUNDLE, EXTRACT_* and APPLY_SETTINGS accept messages from any source or origin if I'm reading this right.
The new org/user checks don't close this path because those expected values come from the same untrusted message. A non-parent sender can provide a valid signed bundle with matching IDs and overwrite the persisted target key or trigger extraction/reset operatioons.
Should mirror #129's legacy wrapper: require the direct parent, reject empty/opaque origins, bind subsequent traffic to the first accepted origin, and restrict responses to that origin. Should also add regression tests for operational messages from non-parent and opaque-origin senders.
…igin messages Mirror PR #129's export-and-sign pattern to the import frame's legacy window.message listener (the path used by @turnkey/iframe-stamper < 2.1.0): 1. Require event.source === window.parent (direct parent only) 2. Reject empty and opaque ('null') origins 3. Bind legacyParentOrigin on the first accepted message; drop later messages from any other origin 4. Call TKHQ.setParentFrameOrigin(event.origin) on first bind so outbound window.parent.postMessage responses are scoped to that origin instead of '*' 5. Skip TURNKEY_INIT_MESSAGE_CHANNEL messages in the legacy listener (owned by the MessageChannel handshake handler) Also export setParentFrameOrigin from import/src/turnkey-core.js and add modulePaths to jest.config.js so @shared/* deps (bech32, etc.) can be resolved from import/node_modules. Adds 11 regression tests in 'Legacy listener cross-origin gate (import frame)' covering: non-parent sender, opaque origin, empty origin, second-origin-after-binding, origin binding/idempotency, and valid parent acceptance. Rebuilds dist bundles.
126d5ae to
ec45a90
Compare
fainashalts
left a comment
There was a problem hiding this comment.
Re-reviewed at ec45a90. Legacy message path is now source/origin-gated, origin-bound, and covered by regression tests. My previous blocker is resolved; no additional blocking issues found.
Summary
Extends the cross-origin
MessageChannelvalidation from INT-697 / PR #129 to the import frame, and closes theorganizationId/userIdbinding gap that allowed legacy callers to skip bundle binding checks.PR #129 hardened the
export-and-signframe'sTURNKEY_INIT_MESSAGE_CHANNELhandler against opaque origins and non-parent senders. Theimportframe had the same pre-existing gap and was not touched by PR #129.Linear: INT-783 https://linear.app/turnkey/issue/INT-783
Security tickets addressed:
ENG-4598 (TKA-20260806-014) https://linear.app/turnkey/issue/ENG-4598 — "The hosted import frame accepts the first
MessageChannelfrom any sender." This PR closes the channel-seizure vector by requiring the sender to be the direct parent with a concrete origin.ENG-4597 - https://linear.app/turnkey/issue/ENG-4597
Fix 1: Cross-origin MessageChannel gate (ENG-4598)
Security ticket: ENG-4598 (TKA-20260806-014) — "The hosted import frame accepts the
first MessageChannel from any sender."
Gate now requires:
event.source === window.parent— only the direct parent frame may establish the channelevent.origin && event.origin !== "null"— opaque/sandboxed origins are rejectedevent.ports?.length === 1— exactly one transferred port requiredFix 2: Import bundle org/user binding (ENG-4597)
Security ticket: ENG-4597 (TKA-20260806-015) — "The hosted import frame accepts signed
import bundles without organization/user binding when legacy callers omit those fields."
onInjectImportBundle()previously logged aconsole.warnand skipped the binding checkwhen
organizationId/userIdwas absent. Both now hard-reject (throw) on missing.Existing mismatch checks unchanged.
Breaking change: iframe-stamper < v2.0.0 (or integrations omitting these fields) now
receive an ERROR response.
Validation
no-consolewarnings (none from this change)45de86a592cd0d80ccd0Reference