fix: prevent proto-access bypass via Symbol.toStringTag and escape bypass via toHTML#2148
fix: prevent proto-access bypass via Symbol.toStringTag and escape bypass via toHTML#2148eddieran wants to merge 2 commits intohandlebars-lang:masterfrom
Conversation
…pass via toHTML - Use instanceof Map/Set instead of Object.prototype.toString tag check, which can be spoofed via Symbol.toStringTag to bypass proto-access controls - Use instanceof SafeString instead of duck-typing toHTML property check, which allows attacker-controlled data to bypass HTML escaping Fixes handlebars-lang#2146
There was a problem hiding this comment.
Pull request overview
Addresses two reported security issues by hardening type checks in Handlebars utility helpers to prevent spoofing-based bypasses.
Changes:
- Replace
isMap/isSettag-based detection withinstanceof Map/instanceof Setchecks. - Replace
escapeExpression’stoHTMLduck-typing with aninstanceof SafeStringcheck.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -45,8 +47,12 @@ | |||
| } | |||
|
|
|||
| export const isArray = Array.isArray; | |||
| export const isMap = testTag('Map'); | |||
| export const isSet = testTag('Set'); | |||
| export function isMap(value) { | |||
There was a problem hiding this comment.
testTag() is now unused after switching isMap/isSet to instanceof, so this helper (and its call sites that were removed) can be deleted to avoid dead code and keep utils.js focused on active helpers.
| if (typeof string !== 'string') { | ||
| // don't escape SafeStrings, since they're already safe | ||
| if (string && string.toHTML) { | ||
| if (string instanceof SafeString) { | ||
| return string.toHTML(); | ||
| } else if (string == null) { |
There was a problem hiding this comment.
This change makes escapeExpression stop treating arbitrary objects with a toHTML property as safe. The existing unit test in spec/utils.js currently asserts that a plain object with toHTML() is not escaped, which will fail and also conflicts with the new security behavior—update that expectation and add a regression test that verifies attacker-controlled toHTML() output is escaped.
| export const isArray = Array.isArray; | ||
| export const isMap = testTag('Map'); | ||
| export const isSet = testTag('Set'); | ||
| export function isMap(value) { | ||
| return value instanceof Map; | ||
| } |
There was a problem hiding this comment.
Given isMap now gates proto-access control behavior in runtime.lookupProperty, it would be good to add a security regression test that a plain object spoofing [Symbol.toStringTag] = 'Map' is not treated as a Map and cannot be used to read blocked proto properties (e.g. constructor). No existing spec appears to cover the spoofing case.
Revert the escapeExpression change (instanceof SafeString) per maintainer review: it's a breaking change to the documented duck-typing contract where any object with .toHTML() is treated as safe. Keep only the Map/Set Symbol.toStringTag spoofing fix. Remove now-unused testTag helper that caused the lint failure.
|
Scope-reduced per @copilot-pull-request-reviewer's note that the |
|
The PR title and description still claim two fixes, but commit 0e4b21 reverted the toHTML → SafeString portion. The final diff is only the Map/Set tag-check fix in lib/handlebars/utils.js. Before merge, the title and body should be updated to match. |
| return value instanceof Set; | ||
| } | ||
|
|
||
| // Older IE versions do not directly support indexOf so we must implement our own, sadly. |
There was a problem hiding this comment.
this needs a regression test, something like
it('does not treat Symbol.toStringTag-spoofed objects as Map', function () {
expectTemplate('{{value.constructor}}')
.withInput({ value: { [Symbol.toStringTag]: 'Map', get: () => 'pwned' } })
.toCompileTo(''); // or whatever the proto-controlled path produces
});There was a problem hiding this comment.
Add one each for isMap / isSet at the unit level (spec/utils.js already has the happy-path tests) plus an end-to-end runtime.js lookup test.
Summary
Fixes #2146. Replaces #2147 (auto-closed when fork was deleted).
Two security fixes in
lib/handlebars/utils.js:1. Proto-access control bypass via
Symbol.toStringTagspoofingisMap/isSetusedObject.prototype.toStringtag detection, which is spoofable viaSymbol.toStringTag. An attacker can craft a plain object that passes the Map check, causinglookupPropertyto skip all proto-access controls and return blocked properties likeconstructor.Fix: Use
instanceof Map/instanceof Setinstead of toString tag comparison.2. HTML escape bypass via
toHTMLduck-typingescapeExpressiontreated any object with atoHTMLproperty as aSafeString, skipping HTML escaping entirely. Attacker-controlled template data with atoHTMLmethod could inject raw HTML into{{}}expressions.Fix: Use
instanceof SafeStringinstead of checking for thetoHTMLproperty.Test plan
{{value}}with{ [Symbol.toStringTag]: 'Map', get: () => 'Function' }no longer bypasses proto-access controls{{value}}with{ toHTML: () => '<img src=x onerror=alert(1)>' }now escapes the HTML outputSafeStringandMapusage continues to work correctlynpm test