Skip to content

fix: stop devtools exceptions from unwinding into the inspected app - #210

Merged
dborysov merged 1 commit into
mainfrom
harden/serializer-boundary
Aug 5, 2026
Merged

fix: stop devtools exceptions from unwinding into the inspected app#210
dborysov merged 1 commit into
mainfrom
harden/serializer-boundary

Conversation

@dborysov

@dborysov dborysov commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #209, which fixed one poison value (invalid Date) but not the
boundary that lets a poison value reach the host app.

sync.content.ts runs in the page's MAIN world and QueryCache.notify iterates
its subscribers without catching, so anything thrown in a subscriber unwinds
into the inspected application's own render/commit and trips its error
boundary, with a stack trace that gives no hint an extension is involved.
Invalid Date was one instance; objects with throwing getters, hostile Proxies
and Object.create(Date.prototype) still reach the same path.

  • guard every callback the page can invoke (both cache subscribers, the
    initial snapshot, the panel message handler, the detection poll) so a
    serialization failure skips one devtools update instead of breaking the page
  • isInvalidDate: read [[DateValue]] via Date.prototype.getTime.call instead of
    the overridable date.getTime(), so the guard agrees with the toISOString it
    protects. An overridden getTime could otherwise hide a NaN time value, or
    throw where the unguarded code did not
  • serializeToJsLiteral: run the emitted date through escapeString like every
    other string in that function. toISOString is page-overridable and this
    output goes to the user's clipboard

Follow-up to #209, which fixed one poison value (invalid Date) but not the
boundary that lets a poison value reach the host app.

sync.content.ts runs in the page's MAIN world and QueryCache.notify iterates
its subscribers without catching, so anything thrown in a subscriber unwinds
into the inspected application's own render/commit and trips its error
boundary, with a stack trace that gives no hint an extension is involved.
Invalid Date was one instance; objects with throwing getters, hostile Proxies
and Object.create(Date.prototype) still reach the same path.

- guard every callback the page can invoke (both cache subscribers, the
  initial snapshot, the panel message handler, the detection poll) so a
  serialization failure skips one devtools update instead of breaking the page
- isInvalidDate: read [[DateValue]] via Date.prototype.getTime.call instead of
  the overridable date.getTime(), so the guard agrees with the toISOString it
  protects. An overridden getTime could otherwise hide a NaN time value, or
  throw where the unguarded code did not
- serializeToJsLiteral: run the emitted date through escapeString like every
  other string in that function. toISOString is page-overridable and this
  output goes to the user's clipboard
@dborysov
dborysov merged commit da4c4ac into main Aug 5, 2026
3 checks passed
@dborysov
dborysov deleted the harden/serializer-boundary branch August 5, 2026 10:01
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