What happened
detachRecordListeners() in chrome_extension/content.js only flips the
recordListenersAttached flag; the comment says as much:
function detachRecordListeners() {
// Note: In practice we'd need to store handler references to properly remove them
// For now, just mark as detached
recordListenersAttached = false;
}
attachRecordListeners() registers click, keydown, keyup, mousemove and scroll
handlers plus three trackMouseEvent handlers with inline/anonymous references, so there is
nothing to pass to removeEventListener. Because setMode calls detachRecordListeners() when
switching to replay or idle, the effect is that the extension keeps capturing after the
user stops recording, and each captured event still goes through
sendMessageToBackgroundScript. Setting record mode again is guarded by
if (!recordListenersAttached), so a stop/start cycle also leaves the original handlers in
place — and a second start attaches a second set only after an intervening reload.
The equivalent replay-side function, disconnectReplayObservers(), does this correctly
(removeEventListener with named handlers, observers disconnected), so the asymmetry looks
like an oversight rather than a decision.
Worth noting the consequence given what the extension captures: keydown/keyup are recorded
per keystroke and every event carries visibleHTMLString (the visible DOM). So "stopped"
still streams keystrokes and page content — including anything typed into a password field,
since the extension has no secret handling — to whatever is listening on the WebSocket. We are
filing this publicly as a functional bug because it is visible in the source and there is no
concealed exploit, but if you would rather have it as a private advisory under SECURITY.md, say
so and we will move it.
Suggested fix
Hoist the handlers to module-level named functions (as the replay path already does) and keep a
list of [target, type, handler, options] triples at attach time to replay in reverse at
detach time. Options must match: the listeners are registered with capture true, and scroll
is on document rather than document.body.
Steps to reproduce
- Load
chrome_extension unpacked and point background.js at a WebSocket listener on
ws://localhost:8765 (a ~10 line echo server is enough).
- Set record mode, click and type on any page — messages arrive.
- Set idle mode.
- Click and type again: messages continue to arrive, and the frames keep carrying
visibleHTMLString.
Environment
Chrome (unpacked, MV3), chrome_extension @ 431fd37, macOS 14.6 arm64.
What happened
detachRecordListeners()inchrome_extension/content.jsonly flips therecordListenersAttachedflag; the comment says as much:attachRecordListeners()registersclick,keydown,keyup,mousemoveandscrollhandlers plus three
trackMouseEventhandlers with inline/anonymous references, so there isnothing to pass to
removeEventListener. BecausesetModecallsdetachRecordListeners()whenswitching to
replayoridle, the effect is that the extension keeps capturing after theuser stops recording, and each captured event still goes through
sendMessageToBackgroundScript. Setting record mode again is guarded byif (!recordListenersAttached), so a stop/start cycle also leaves the original handlers inplace — and a second start attaches a second set only after an intervening reload.
The equivalent replay-side function,
disconnectReplayObservers(), does this correctly(
removeEventListenerwith named handlers, observers disconnected), so the asymmetry lookslike an oversight rather than a decision.
Worth noting the consequence given what the extension captures:
keydown/keyupare recordedper keystroke and every event carries
visibleHTMLString(the visible DOM). So "stopped"still streams keystrokes and page content — including anything typed into a password field,
since the extension has no secret handling — to whatever is listening on the WebSocket. We are
filing this publicly as a functional bug because it is visible in the source and there is no
concealed exploit, but if you would rather have it as a private advisory under SECURITY.md, say
so and we will move it.
Suggested fix
Hoist the handlers to module-level named functions (as the replay path already does) and keep a
list of
[target, type, handler, options]triples at attach time to replay in reverse atdetach time. Options must match: the listeners are registered with capture
true, andscrollis on
documentrather thandocument.body.Steps to reproduce
chrome_extensionunpacked and pointbackground.jsat a WebSocket listener onws://localhost:8765(a ~10 line echo server is enough).visibleHTMLString.Environment
Chrome (unpacked, MV3),
chrome_extension@431fd37, macOS 14.6 arm64.