feat(web-security): postMessage listener discovery and origin-validation depth - #126
Merged
GangGreenTemperTatum merged 1 commit intoAug 20, 2026
Conversation
…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).
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.
Across all 82 skills, these appeared zero times:
Our skills told an agent to grep
addEventListener("message", ...). That silently misses:window.onmessage = fnsetter assignment.MessagePort/MessageChannelhandlers (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:/^https:\/\/trusted.example\.com$/https://trustedXexample.com$anchor/^https:\/\/trusted\.example\.com/https://trusted.example.com.evil.tldAlso flags wildcard
targetOriginon the reply path — leaks even when the inbound check is sound.Two errors caught during review (worth flagging)
rgcommand 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 realrgand asserts it matches the 7 unsafe lines while skipping strict===.native codecheck was overstated. I'd presentedaddEventListener.toString().includes('native code')as a detection. It's trivially spoofable by overridingtoString(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:Also corrected against upstream source: Rollbar tries
_wrappedbefore_rollbar_wrapped, Raven unwraps via its single function-valued property, and Bugsnag is not recoverable (upstream dropped the deprecatedcallee.callerchain) — my first draft had all three wrong.Tests
522 passing (+43). Assertions are behavioural, not textual:
rgpattern is compiled and executedMutation-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 recoverableappeared twice so corrupting one row still passed.Test plan
pytest tests/→ 522 passeddreadnode capability validate --strict→ 0 failed (only environmentalcaido-cli/burpchecks)ruff check --select E9,F63,F7,F82→ cleanast-grep/rgcommands executed against realistic JS