From cac7e3bd4d41fb4eb1e4b5815cca2ce17dc7715a Mon Sep 17 00:00:00 2001 From: ran Date: Sun, 12 Apr 2026 20:50:23 +0800 Subject: [PATCH 1/2] fix: prevent proto-access bypass via Symbol.toStringTag and escape bypass 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 #2146 --- lib/handlebars/utils.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index dc9812fc0..dd23de09e 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -1,3 +1,5 @@ +import SafeString from './safe-string'; + const escape = { '&': '&', '<': '<', @@ -45,8 +47,12 @@ function testTag(name) { } export const isArray = Array.isArray; -export const isMap = testTag('Map'); -export const isSet = testTag('Set'); +export function isMap(value) { + return value instanceof Map; +} +export function isSet(value) { + return value instanceof Set; +} // Older IE versions do not directly support indexOf so we must implement our own, sadly. export function indexOf(array, value) { @@ -61,7 +67,7 @@ export function indexOf(array, value) { export function escapeExpression(string) { 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) { return ''; From 0e4b21264c487dbad17b0e6db7d048173b5586ac Mon Sep 17 00:00:00 2001 From: Ran Date: Fri, 17 Apr 2026 06:10:47 +0800 Subject: [PATCH 2/2] fix: scope PR to Map/Set spoofing fix only 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. --- lib/handlebars/utils.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index dd23de09e..ad141e9c7 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -1,5 +1,3 @@ -import SafeString from './safe-string'; - const escape = { '&': '&', '<': '<', @@ -37,15 +35,6 @@ export function isFunction(value) { return typeof value === 'function'; } -function testTag(name) { - const tag = '[object ' + name + ']'; - return function (value) { - return value && typeof value === 'object' - ? toString.call(value) === tag - : false; - }; -} - export const isArray = Array.isArray; export function isMap(value) { return value instanceof Map; @@ -67,7 +56,7 @@ export function indexOf(array, value) { export function escapeExpression(string) { if (typeof string !== 'string') { // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { + if (string && string.toHTML) { return string.toHTML(); } else if (string == null) { return '';