Skip to content

feat(web-security): postMessage listener discovery and origin-validation depth - #126

Merged
GangGreenTemperTatum merged 1 commit into
mainfrom
ads/eng-8078-update-web-security-dom-capabilities-for-cspts-and
Aug 20, 2026
Merged

feat(web-security): postMessage listener discovery and origin-validation depth#126
GangGreenTemperTatum merged 1 commit into
mainfrom
ads/eng-8078-update-web-security-dom-capabilities-for-cspts-and

Conversation

@GangGreenTemperTatum

@GangGreenTemperTatum GangGreenTemperTatum commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Across all 82 skills, these appeared zero times:

MessagePort · MessageChannel · __lookupSetter__ · nr@original · __sentry_original__ · zone.js

Our skills told an agent to grep addEventListener("message", ...). That silently misses:

  • Wrapper-hidden handlers. Sentry/New Relic/Rollbar/Raven/Bugsnag/Zone.js wrap listeners, so the registered function is the monitoring shim — reviewing it reads the wrong code entirely.
  • window.onmessage = fn setter assignment.
  • MessagePort/MessageChannel handlers (iframe & worker bridges, SDKs).

Adds a wrapper tell → recover-from table verified line-by-line against src/main.ts, plus a runtime capture snippet hooking all three surfaces.

Origin validation depth

Previously we named only startsWith/endsWith. Now the full set — indexOf, includes, search, startsWith, endsWith, loose ==/!= — plus two regex bugs that read as correct:

Bug Example Bypass
Unescaped dot /^https:\/\/trusted.example\.com$/ https://trustedXexample.com
Missing $ anchor /^https:\/\/trusted\.example\.com/ https://trusted.example.com.evil.tld

Also flags wildcard targetOrigin on the reply path — leaks even when the inbound check is sound.

Two errors caught during review (worth flagging)

  1. The documented rg command didn't run. My weak-origin pattern used (?!=) lookahead, which ripgrep's default engine rejects outright (error: look-around ... is not supported). Replaced with a portable [^=] form, and there's now a test that shells out to real rg and asserts it matches the 7 unsafe lines while skipping strict ===.
  2. The native code check was overstated. I'd presented addEventListener.toString().includes('native code') as a detection. It's trivially spoofable by overriding toString (demonstrated) and also trips on unrelated extensions. Now qualified as a weak signal, and replaced with a snippet that genuinely enumerates listeners — verified in headless Chromium capturing all three surfaces:
    ["addEventListener:appHandler","onmessage:inlineHandler","MessagePort:portHandler"]
    

Also corrected against upstream source: Rollbar tries _wrapped before _rollbar_wrapped, Raven unwraps via its single function-valued property, and Bugsnag is not recoverable (upstream dropped the deprecated callee.caller chain) — my first draft had all three wrong.

Tests

522 passing (+43). Assertions are behavioural, not textual:

  • every documented rg pattern is compiled and executed
  • every origin bypass in the table is actually run
  • wrapper claims are checked against upstream source
  • all 10 rule IDs and the example output verified against the live engine

Mutation-tested. Deleting any of the three new sections fails 6–7 tests. Two guards were tightened after mutations initially slipped through: an operator name matched prose outside its table, and not recoverable appeared twice so corrupting one row still passed.

Test plan

  • pytest tests/ → 522 passed
  • dreadnode capability validate --strict → 0 failed (only environmental caido-cli/burp checks)
  • ruff check --select E9,F63,F7,F82 → clean
  • Both skills load through the real Dreadnode skill loader
  • Runtime capture snippet verified in headless Chromium
  • All ast-grep/rg commands executed against realistic JS

…ion depth

Distils the transferable intelligence from FransyTracker
(gitlab.com/joaxcar/fransytracker, MIT) into the two DOM skills. The extension
itself is a human-driven DevTools panel and is deliberately NOT wrapped as a
tool -- the value is the knowledge, not the UI.

Listener discovery (the biggest gap)
Across all 82 skills, `MessagePort`, `MessageChannel`, `__lookupSetter__`,
`nr@original`, `__sentry_original__` and `zone.js` appeared ZERO times. Our
skills told an agent to grep addEventListener("message", ...), which silently
misses:
  - handlers wrapped by Sentry/New Relic/Rollbar/Raven/Bugsnag/Zone.js, where
    the registered function is the monitoring shim and reading it reviews the
    wrong code
  - `window.onmessage = fn` setter assignment
  - MessagePort/MessageChannel handlers
Adds a wrapper "tell -> recover from" table verified line-by-line against
src/main.ts, including that Rollbar tries _wrapped before _rollbar_wrapped and
that Bugsnag is NOT recoverable (upstream dropped the callee.caller chain), and
a runtime capture snippet hooking all three surfaces.

Origin validation
Adds the full weak-operator set -- indexOf / includes / search / startsWith /
endsWith / loose equality -- where we previously named only two, plus two regex
bugs that read as correct: an unescaped dot (matches any char) and a missing $
anchor (allows any suffix). Every bypass in the table is executed in the tests
rather than asserted. Also flags wildcard targetOrigin on the *reply* path,
which leaks even when the inbound origin check is sound.

FransyTracker as optional triage
Its rules engine is a self-contained IIFE with no Chrome/DOM dependency, so it
runs standalone under tsx over harvested listener bodies. Documented with its
MEASURED blind spots -- e.source.postMessage, destructuring, two-hop aliases,
jQuery .html(), setTimeout(string) are all unflagged -- so a clean result reads
as "not yet triaged", never "safe". Includes the host_permissions *://*/*
opsec warning and full credit to Frans Rosen and Zeetaz.

Corrections made during review
  - the documented weak-origin `rg` used (?!=) lookahead, which ripgrep's
    default engine rejects outright; replaced with a portable [^=] form and a
    test that runs rg for real
  - the "native code" hook check was presented as evidence; it is trivially
    spoofable by overriding toString and also trips on unrelated extensions.
    Now qualified as a weak signal, with a snippet that actually enumerates
    listeners (verified in headless Chromium: all three surfaces captured)

Tests: 522 passing (+43). Assertions are behavioural -- rg patterns are
compiled and executed, every origin bypass is run, wrapper claims are checked
against upstream source. Mutation-tested: deleting any of the three new
sections fails 6-7 tests, and two guards were tightened after mutations
initially slipped through (an operator name matched prose outside its table;
"not recoverable" appeared twice so replacing one still passed).
@GangGreenTemperTatum
GangGreenTemperTatum merged commit daf4f73 into main Aug 20, 2026
5 checks passed
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