fix(html): make raw() trust marker unforgeable and hoist escape map#1473
Conversation
Use a module-private, unregistered symbol for the `raw()` trust marker
instead of `Symbol.for("h3.rawHTML")`. The global registry key was
discoverable, so any object carrying it (a second h3 realm, third-party
code, or a plain `Symbol.for(...)` call) could forge a trusted RawHTML
value and bypass escaping in the `html\`\`` tagged template.
Also hoist the escape-character map in `escapeHtml` to a module-level
constant so it is no longer reallocated for every matched character on
the per-request render path. Behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe trusted HTML marker is now module-local instead of globally registered, tests reject forged markers, and HTML escaping uses a shared lookup table. ChangesTrusted HTML handling
Estimated code review effort: 1 (Trivial) | ~4 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Follow-up fixes for the HTML-escaping change in #1459 (
html/raw/escapeHtml).1.
raw()trust marker was globally forgeable (security)raw()tagged trusted HTML withSymbol.for("h3.rawHTML")— a key from the global symbol registry. Because the key is discoverable, any object carrying it (Symbol.for("h3.rawHTML")set totrue) could forge a trustedRawHTMLvalue and be rendered unescaped by thehtml`` tagged template, re-introducing XSS.Switched to a module-private, unregistered
Symbol("h3.rawHTML")so the marker cannot be reproduced outside this module.This is a defense-in-depth hardening: plain string/JSON user input can't carry symbol keys, so it requires an object bearing the symbol to reach an interpolation — but it's cheap to close.
2.
escapeHtmlreallocated the replacement map per matched char (efficiency)The 5-entry map object literal was built inside the
replacecallback, allocating a fresh object for every special character matched.escapeHtmlruns once per interpolated value on the per-request render path. Hoisted it to a module-level constant. Behavior unchanged.Tests
Updated the existing test that asserted the forgeable behavior as a feature — it now asserts a forged
Symbol.for("h3.rawHTML")object is escaped rather than trusted. Full suite + lint pass.🤖 Generated with Claude Code
Summary by CodeRabbit