VYPR
High severity7.5NVD Advisory· Published Mar 27, 2026· Updated Mar 31, 2026

CVE-2026-33939

CVE-2026-33939

Description

Handlebars provides the power necessary to let users build semantic templates. In versions 4.0.0 through 4.7.8, when a Handlebars template contains decorator syntax referencing an unregistered decorator (e.g. {{*n}}), the compiled template calls lookupProperty(decorators, "n"), which returns undefined. The runtime then immediately invokes the result as a function, causing an unhandled TypeError: ... is not a function that crashes the Node.js process. Any application that compiles user-supplied templates without wrapping the call in a try/catch is vulnerable to a single-request Denial of Service. Version 4.7.9 fixes the issue. Some workarounds are available. Wrap compilation and rendering in try/catch. Validate template input before passing it to compile(); reject templates containing decorator syntax ({{*...}}) if decorators are not used in your application. Use the pre-compilation workflow; compile templates at build time and serve only pre-compiled templates; do not call compile() at request time.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
handlebarsnpm
>= 4.0.0, < 4.7.94.7.9

Affected products

1

Patches

1
68d8df5a88e0

Fix security issues

https://github.com/handlebars-lang/handlebars.jsJakob LinskesederMar 24, 2026via ghsa
20 files changed · +541 38
  • lib/handlebars/compiler/base.js+67 0 modified
    @@ -1,6 +1,7 @@
     import parser from './parser';
     import WhitespaceControl from './whitespace-control';
     import * as Helpers from './helpers';
    +import Exception from '../exception';
     import { extend } from '../utils';
     
     export { parser };
    @@ -11,6 +12,9 @@ extend(yy, Helpers);
     export function parseWithoutProcessing(input, options) {
       // Just return if an already-compiled AST was passed in.
       if (input.type === 'Program') {
    +    // When a pre-parsed AST is passed in, validate all node values to prevent
    +    // code injection via type-confused literals.
    +    validateInputAst(input);
         return input;
       }
     
    @@ -32,3 +36,66 @@ export function parse(input, options) {
     
       return strip.accept(ast);
     }
    +
    +function validateInputAst(ast) {
    +  validateAstNode(ast);
    +}
    +
    +function validateAstNode(node) {
    +  if (node == null) {
    +    return;
    +  }
    +
    +  if (Array.isArray(node)) {
    +    node.forEach(validateAstNode);
    +    return;
    +  }
    +
    +  if (typeof node !== 'object') {
    +    return;
    +  }
    +
    +  if (node.type === 'PathExpression') {
    +    if (!isValidDepth(node.depth)) {
    +      throw new Exception(
    +        'Invalid AST: PathExpression.depth must be an integer'
    +      );
    +    }
    +    if (!Array.isArray(node.parts)) {
    +      throw new Exception('Invalid AST: PathExpression.parts must be an array');
    +    }
    +    for (let i = 0; i < node.parts.length; i++) {
    +      if (typeof node.parts[i] !== 'string') {
    +        throw new Exception(
    +          'Invalid AST: PathExpression.parts must only contain strings'
    +        );
    +      }
    +    }
    +  } else if (node.type === 'NumberLiteral') {
    +    if (typeof node.value !== 'number' || !isFinite(node.value)) {
    +      throw new Exception('Invalid AST: NumberLiteral.value must be a number');
    +    }
    +  } else if (node.type === 'BooleanLiteral') {
    +    if (typeof node.value !== 'boolean') {
    +      throw new Exception(
    +        'Invalid AST: BooleanLiteral.value must be a boolean'
    +      );
    +    }
    +  }
    +
    +  Object.keys(node).forEach(propertyName => {
    +    if (propertyName === 'loc') {
    +      return;
    +    }
    +    validateAstNode(node[propertyName]);
    +  });
    +}
    +
    +function isValidDepth(depth) {
    +  return (
    +    typeof depth === 'number' &&
    +    isFinite(depth) &&
    +    Math.floor(depth) === depth &&
    +    depth >= 0
    +  );
    +}
    
  • lib/handlebars/compiler/javascript-compiler.js+10 1 modified
    @@ -686,9 +686,18 @@ JavaScriptCompiler.prototype = {
         let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
           options = this.setupHelperArgs(name, paramSize);
     
    +    // Store the resolved decorator in a variable and verify it is a function before
    +    // calling it. Without this, unregistered decorators can cause an unhandled TypeError
    +    // (calling undefined), which crashes the process — enabling Denial of Service.
    +    this.decorators.push(['var decorator = ', foundDecorator, ';']);
    +    this.decorators.push([
    +      'if (typeof decorator !== "function") { throw new Error(',
    +      this.quotedString('Missing decorator: "' + name + '"'),
    +      '); }'
    +    ]);
         this.decorators.push([
           'fn = ',
    -      this.decorators.functionCall(foundDecorator, '', [
    +      this.decorators.functionCall('decorator', '', [
             'fn',
             'props',
             'container',
    
  • lib/handlebars/internal/proto-access.js+1 0 modified
    @@ -16,6 +16,7 @@ export function createProtoAccessControl(runtimeOptions) {
       methodWhiteList['__defineGetter__'] = false;
       methodWhiteList['__defineSetter__'] = false;
       methodWhiteList['__lookupGetter__'] = false;
    +  methodWhiteList['__lookupSetter__'] = false;
       extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
     
       return {
    
  • lib/handlebars/runtime.js+11 5 modified
    @@ -138,7 +138,7 @@ export function template(templateSpec, env) {
           for (let i = 0; i < len; i++) {
             let result = depths[i] && container.lookupProperty(depths[i], name);
             if (result != null) {
    -          return depths[i][name];
    +          return result;
             }
           }
         },
    @@ -349,21 +349,21 @@ export function wrapProgram(
     export function resolvePartial(partial, context, options) {
       if (!partial) {
         if (options.name === '@partial-block') {
    -      partial = options.data['partial-block'];
    +      partial = lookupOwnProperty(options.data, 'partial-block');
         } else {
    -      partial = options.partials[options.name];
    +      partial = lookupOwnProperty(options.partials, options.name);
         }
       } else if (!partial.call && !options.name) {
         // This is a dynamic partial that returned a string
         options.name = partial;
    -    partial = options.partials[partial];
    +    partial = lookupOwnProperty(options.partials, partial);
       }
       return partial;
     }
     
     export function invokePartial(partial, context, options) {
       // Use the current closure context to save the partial-block if this partial
    -  const currentPartialBlock = options.data && options.data['partial-block'];
    +  const currentPartialBlock = lookupOwnProperty(options.data, 'partial-block');
       options.partial = true;
       if (options.ids) {
         options.data.contextPath = options.ids[0] || options.data.contextPath;
    @@ -404,6 +404,12 @@ export function noop() {
       return '';
     }
     
    +function lookupOwnProperty(obj, name) {
    +  if (obj && Object.prototype.hasOwnProperty.call(obj, name)) {
    +    return obj[name];
    +  }
    +}
    +
     function initData(context, data) {
       if (!data || !('root' in data)) {
         data = data ? createFrame(data) : {};
    
  • lib/precompiler.js+45 8 modified
    @@ -196,16 +196,24 @@ module.exports.cli = function(opts) {
     
       const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
     
    +  if (opts.namespace && !isValidNamespace(opts.namespace)) {
    +    throw new Handlebars.Exception('Invalid namespace format');
    +  }
    +
       let output = new SourceNode();
       if (!opts.simple) {
         if (opts.amd) {
    +      const runtimeModulePath =
    +        (opts.handlebarPath || '') + 'handlebars.runtime';
           output.add(
    -        "define(['" +
    -          opts.handlebarPath +
    -          'handlebars.runtime\'], function(Handlebars) {\n  Handlebars = Handlebars["default"];'
    +        'define([' +
    +          quoteForJavaScript(runtimeModulePath) +
    +          '], function(Handlebars) {\n  Handlebars = Handlebars["default"];'
           );
         } else if (opts.commonjs) {
    -      output.add('var Handlebars = require("' + opts.commonjs + '");');
    +      output.add(
    +        'var Handlebars = require(' + quoteForJavaScript(opts.commonjs) + ');'
    +      );
         } else {
           output.add('(function() {\n');
         }
    @@ -255,9 +263,9 @@ module.exports.cli = function(opts) {
           }
           output.add([
             objectName,
    -        "['",
    -        template.name,
    -        "'] = template(",
    +        '[',
    +        quoteForJavaScript(template.name),
    +        '] = template(',
             precompiled,
             ');\n'
           ]);
    @@ -277,7 +285,9 @@ module.exports.cli = function(opts) {
       }
     
       if (opts.map) {
    -    output.add('\n//# sourceMappingURL=' + opts.map + '\n');
    +    output.add(
    +      '\n//# sourceMappingURL=' + sanitizeSourceMapComment(opts.map) + '\n'
    +    );
       }
     
       output = output.toStringWithSourceMap();
    @@ -307,6 +317,33 @@ function arrayCast(value) {
       return value;
     }
     
    +/*
    + * Safely quotes a value for embedding in generated JavaScript strings
    + *
    + * Uses JSON.stringify which handles all special characters.
    + */
    +function quoteForJavaScript(value) {
    +  return JSON.stringify(String(value));
    +}
    +
    +/**
    + * Validates that a namespace is a legitimate dotted JavaScript identifier
    + * (e.g. "App.templates") to prevent arbitrary code injection
    + */
    +function isValidNamespace(namespace) {
    +  return /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(
    +    namespace
    +  );
    +}
    +
    +/**
    + * Strips line terminators from source map URLs to prevent injection of new
    + * JavaScript lines via the sourceMappingURL comment
    + */
    +function sanitizeSourceMapComment(value) {
    +  return String(value).replace(/[\r\n\u2028\u2029]/g, '');
    +}
    +
     /**
      * Run uglify to minify the compiled template, if uglify exists in the dependencies.
      *
    
  • spec/compiler.js+140 0 modified
    @@ -128,6 +128,146 @@ describe('compiler', function() {
           );
         });
     
    +    it('should reject AST with invalid PathExpression depth', function() {
    +      shouldThrow(
    +        function() {
    +          Handlebars.compile({
    +            type: 'Program',
    +            body: [
    +              {
    +                type: 'MustacheStatement',
    +                escaped: true,
    +                strip: { open: false, close: false },
    +                path: {
    +                  type: 'PathExpression',
    +                  data: false,
    +                  depth: '0',
    +                  parts: ['this'],
    +                  original: 'this'
    +                },
    +                params: []
    +              }
    +            ]
    +          })();
    +        },
    +        Error,
    +        'Invalid AST: PathExpression.depth must be an integer'
    +      );
    +    });
    +
    +    it('should reject AST with non-array PathExpression parts', function() {
    +      shouldThrow(
    +        function() {
    +          Handlebars.compile({
    +            type: 'Program',
    +            body: [
    +              {
    +                type: 'MustacheStatement',
    +                escaped: true,
    +                strip: { open: false, close: false },
    +                path: {
    +                  type: 'PathExpression',
    +                  data: false,
    +                  depth: 0,
    +                  parts: 'this',
    +                  original: 'this'
    +                },
    +                params: []
    +              }
    +            ]
    +          })();
    +        },
    +        Error,
    +        'Invalid AST: PathExpression.parts must be an array'
    +      );
    +    });
    +
    +    it('should reject AST with non-string PathExpression part', function() {
    +      shouldThrow(
    +        function() {
    +          Handlebars.compile({
    +            type: 'Program',
    +            body: [
    +              {
    +                type: 'MustacheStatement',
    +                escaped: true,
    +                strip: { open: false, close: false },
    +                path: {
    +                  type: 'PathExpression',
    +                  data: false,
    +                  depth: 0,
    +                  parts: [1],
    +                  original: 'this'
    +                },
    +                params: []
    +              }
    +            ]
    +          })();
    +        },
    +        Error,
    +        'Invalid AST: PathExpression.parts must only contain strings'
    +      );
    +    });
    +
    +    it('should reject AST with invalid BooleanLiteral value type', function() {
    +      shouldThrow(
    +        function() {
    +          Handlebars.compile({
    +            type: 'Program',
    +            body: [
    +              {
    +                type: 'MustacheStatement',
    +                escaped: true,
    +                strip: { open: false, close: false },
    +                path: {
    +                  type: 'PathExpression',
    +                  data: false,
    +                  depth: 0,
    +                  parts: ['if'],
    +                  original: 'if'
    +                },
    +                params: [
    +                  {
    +                    type: 'BooleanLiteral',
    +                    value: 'true',
    +                    original: true
    +                  }
    +                ]
    +              }
    +            ]
    +          })();
    +        },
    +        Error,
    +        'Invalid AST: BooleanLiteral.value must be a boolean'
    +      );
    +    });
    +
    +    it('should ignore loc metadata while validating AST nodes', function() {
    +      equal(
    +        Handlebars.compile({
    +          type: 'Program',
    +          meta: null,
    +          loc: { source: 'fake', start: { line: 1, column: 0 } },
    +          body: [{ type: 'ContentStatement', value: 'Hello' }]
    +        })(),
    +        'Hello'
    +      );
    +    });
    +
    +    it('should accept AST with valid NumberLiteral values', function() {
    +      equal(
    +        Handlebars.compile(Handlebars.parse('{{lookup this 1}}'))(['a', 'b']),
    +        'b'
    +      );
    +    });
    +
    +    it('should accept AST with valid BooleanLiteral values', function() {
    +      equal(
    +        Handlebars.compile(Handlebars.parse('{{#if true}}ok{{/if}}'))({}),
    +        'ok'
    +      );
    +    });
    +
         it('can pass through an empty string', function() {
           equal(Handlebars.compile('')(), '');
         });
    
  • spec/expected/bom.amd.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
         Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -  return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +  return templates["bom"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
           return "a";
       },"useData":true});
       });
    \ No newline at end of file
    
  • spec/expected/empty.amd.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
       Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
         return "";
     },"useData":true});
     });
    
  • spec/expected/empty.amd.namespace.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
       Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
    -return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
         return "";
     },"useData":true});
     });
    
  • spec/expected/empty.common.js+1 1 modified
    @@ -1,6 +1,6 @@
     (function() {
       var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
         return "";
     },"useData":true});
     })();
    \ No newline at end of file
    
  • spec/expected/empty.name.amd.js+3 3 modified
    @@ -1,9 +1,9 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
       Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +templates["firstTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
         return "<div>1</div>";
     },"useData":true});
    -templates['secondTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +templates["secondTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
         return "<div>2</div>";
     },"useData":true});
     return templates;
    
  • spec/expected/empty.root.amd.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
       Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -return templates['artifacts/partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +return templates["artifacts/partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
         return "<div>Test Partial</div>";
     },"useData":true});
     });
    \ No newline at end of file
    
  • spec/expected/handlebar.path.amd.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['some-path/handlebars.runtime'], function(Handlebars) {
    +define(["some-path/handlebars.runtime"], function(Handlebars) {
         Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -  return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +  return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
           return "";
       },"useData":true});
       });
    \ No newline at end of file
    
  • spec/expected/namespace.amd.js+3 3 modified
    @@ -1,9 +1,9 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
         Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = someNameSpace = someNameSpace || {};
    -  templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +  templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
           return "";
       },"useData":true});
    -  templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +  templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
           return "";
       },"useData":true});
       return templates;
    
  • spec/expected/non.default.extension.amd.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
         Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -  return templates['non.default.extension'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +  return templates["non.default.extension"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
           return "<div>This is a test</div>";
       },"useData":true});
       });
    \ No newline at end of file
    
  • spec/expected/non.empty.amd.known.helper.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
     Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -return templates['known.helpers'] = template({"0":function(container,depth0,helpers,partials,data) {
    +return templates["known.helpers"] = template({"0":function(container,depth0,helpers,partials,data) {
     var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
     if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
     return parent[propertyName];
    
  • spec/expected/partial.template.js+2 2 modified
    @@ -1,6 +1,6 @@
    -define(['handlebars.runtime'], function(Handlebars) {
    +define(["handlebars.runtime"], function(Handlebars) {
         Handlebars = Handlebars["default"];  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
    -  return Handlebars.partials['partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    +  return Handlebars.partials["partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
           return "<div>Test Partial</div>";
       },"useData":true});
       });
    \ No newline at end of file
    
  • spec/precompiler.js+82 1 modified
    @@ -182,7 +182,7 @@ describe('precompiler', function() {
           return 'amd';
         };
         Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
    -    equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
    +    equal(/return Handlebars\.partials\["empty"\]/.test(log), true);
         equal(/template\(amd\)/.test(log), true);
       });
       it('should output multiple amd partials', function() {
    @@ -405,4 +405,85 @@ describe('precompiler', function() {
           });
         });
       });
    +
    +  describe('GHSA-xjpj-3mr7-gcpf: precompiler output escaping', function() {
    +    var FullHandlebars = require('../dist/cjs/handlebars')['default'];
    +
    +    function runCliAndCaptureOutput(options) {
    +      var output = '';
    +      var oldLog = console.log;
    +      console.log = function() {
    +        output += Array.prototype.join.call(arguments, '');
    +      };
    +
    +      try {
    +        Precompiler.cli(options);
    +      } finally {
    +        console.log = oldLog;
    +      }
    +
    +      return output;
    +    }
    +
    +    it('should not inject raw template names into generated code', function() {
    +      var output = runCliAndCaptureOutput({
    +        templates: [
    +          {
    +            name: "evil'];global.__xjpjName=1;//",
    +            source: ''
    +          }
    +        ],
    +        amd: true
    +      });
    +
    +      expect(output).to.not.match(/\['evil'\];global\.__xjpjName=1/);
    +    });
    +
    +    it('should not inject raw commonjs option values into generated code', function() {
    +      var output = runCliAndCaptureOutput({
    +        templates: [{ name: 'safe', source: '' }],
    +        commonjs: 'handlebars");global.__xjpjCommon=1;//'
    +      });
    +
    +      expect(output).to.not.match(
    +        /require\("handlebars"\);global\.__xjpjCommon=1/
    +      );
    +    });
    +
    +    it('should reject invalid namespace expressions', function() {
    +      expect(function() {
    +        runCliAndCaptureOutput({
    +          templates: [{ name: 'safe', source: '' }],
    +          namespace: 'App.ns;global.__xjpjNamespace=1;//'
    +        });
    +      }).to.throw(/Invalid namespace/);
    +    });
    +
    +    it('should sanitize sourceMappingURL comment values', function() {
    +      var oldPrecompile = FullHandlebars.precompile;
    +      var oldWriteFileSync = fs.writeFileSync;
    +      FullHandlebars.precompile = function() {
    +        return {
    +          code: '""',
    +          map: '{"version":3,"sources":[],"names":[],"mappings":""}'
    +        };
    +      };
    +      fs.writeFileSync = function() {};
    +
    +      var output;
    +      try {
    +        output = runCliAndCaptureOutput({
    +          templates: [{ name: 'safe', source: '' }],
    +          map: 'good.js.map\n;global.__xjpjMap=1;//'
    +        });
    +      } finally {
    +        FullHandlebars.precompile = oldPrecompile;
    +        fs.writeFileSync = oldWriteFileSync;
    +      }
    +
    +      expect(output).to.not.match(
    +        /sourceMappingURL=[^\n]*\n;global\.__xjpjMap=1/
    +      );
    +    });
    +  });
     });
    
  • spec/runtime.js+7 0 modified
    @@ -54,6 +54,13 @@ describe('runtime', function() {
             /Template was precompiled with an older version of Handlebars than the current runtime/
           );
         });
    +
    +    it('should safely resolve missing partial map entries', function() {
    +      equal(
    +        Handlebars.VM.resolvePartial(undefined, {}, { name: 'missing' }),
    +        undefined
    +      );
    +    });
       });
     
       describe('#child', function() {
    
  • spec/security.js+155 0 modified
    @@ -133,11 +133,13 @@ describe('security issues', function() {
           '{{__defineGetter__}}',
           '{{__defineSetter__}}',
           '{{__lookupGetter__}}',
    +      '{{__lookupSetter__}}',
           '{{__proto__}}',
           '{{lookup this "constructor"}}',
           '{{lookup this "__defineGetter__"}}',
           '{{lookup this "__defineSetter__"}}',
           '{{lookup this "__lookupGetter__"}}',
    +      '{{lookup this "__lookupSetter__"}}',
           '{{lookup this "__proto__"}}'
         ];
     
    @@ -422,6 +424,159 @@ describe('security issues', function() {
             .toCompileTo('c');
         });
       });
    +
    +  describe('GHSA-2qvq-rjwj-gvw9: partial resolution must not use polluted prototypes', function() {
    +    if (!Handlebars.compile) {
    +      return;
    +    }
    +
    +    afterEach(function() {
    +      delete Object.prototype.widget;
    +    });
    +
    +    it('should not resolve partial names from Object.prototype', function() {
    +      // eslint-disable-next-line no-extend-native
    +      Object.prototype.widget = '<img src=x onerror="alert(1)">';
    +
    +      expect(function() {
    +        Handlebars.compile('<div>{{> widget}}</div>')({});
    +      }).to.throw(/could not be found/);
    +    });
    +  });
    +
    +  describe('GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r: untrusted AST inputs', function() {
    +    if (!Handlebars.compile) {
    +      return;
    +    }
    +
    +    function createInjectedProgram() {
    +      return {
    +        type: 'Program',
    +        body: [
    +          {
    +            type: 'MustacheStatement',
    +            escaped: true,
    +            strip: {
    +              open: false,
    +              close: false
    +            },
    +            path: {
    +              type: 'PathExpression',
    +              data: false,
    +              depth: 0,
    +              parts: ['lookup'],
    +              original: 'lookup'
    +            },
    +            params: [
    +              {
    +                type: 'PathExpression',
    +                data: false,
    +                depth: 0,
    +                parts: [],
    +                original: 'this'
    +              },
    +              {
    +                type: 'NumberLiteral',
    +                value: '{},{})) + (Function) + (({}',
    +                original: 1
    +              }
    +            ]
    +          }
    +        ]
    +      };
    +    }
    +
    +    it('should reject AST NumberLiteral type confusion in compile()', function() {
    +      expect(function() {
    +        var template = Handlebars.compile(createInjectedProgram());
    +        template({});
    +      }).to.throw(/Invalid AST/);
    +    });
    +
    +    it('should reject AST objects passed via dynamic partial lookup', function() {
    +      expect(function() {
    +        var template = Handlebars.compile('{{> (lookup . "payload")}}');
    +        template({
    +          payload: createInjectedProgram()
    +        });
    +      }).to.throw(/Invalid AST|could not be found/);
    +    });
    +  });
    +
    +  describe('GHSA-442j-39wm-28r2: lookup must return checked value', function() {
    +    it('should use the validated value from lookupProperty() in compat mode', function() {
    +      var input = { child: {} };
    +      var readCount = 0;
    +      Object.defineProperty(input, 'unstable', {
    +        enumerable: true,
    +        get: function() {
    +          readCount++;
    +          return readCount === 1 ? 'first-read' : 'second-read';
    +        }
    +      });
    +
    +      expectTemplate('{{#with child}}{{unstable}}{{/with}}')
    +        .withInput(input)
    +        .withCompileOptions({ compat: true })
    +        .toCompileTo('first-read');
    +    });
    +  });
    +
    +  describe('GHSA-9cx6-37pm-9jff: malformed decorators should fail safely', function() {
    +    if (!Handlebars.compile) {
    +      return;
    +    }
    +
    +    it('should throw a controlled error for unknown decorators', function() {
    +      var template = Handlebars.compile('{{*notRegistered}}');
    +      expect(function() {
    +        template({});
    +      }).to.throw(/Missing decorator|not registered/);
    +    });
    +  });
    +
    +  describe('GHSA-new: @partial-block must not resolve from polluted prototype', function() {
    +    if (!Handlebars.compile) {
    +      return;
    +    }
    +
    +    afterEach(function() {
    +      delete Object.prototype['partial-block'];
    +    });
    +
    +    it('should not resolve @partial-block from Object.prototype', function() {
    +      // eslint-disable-next-line no-extend-native
    +      Object.prototype['partial-block'] = '<img src=x onerror="alert(1)">';
    +
    +      expect(function() {
    +        Handlebars.compile('{{> @partial-block}}')({});
    +      }).to.throw(/could not be found/);
    +    });
    +
    +    it('should not resolve @partial-block from Object.prototype inside a partial', function() {
    +      // eslint-disable-next-line no-extend-native
    +      Object.prototype['partial-block'] = '<img src=x onerror="alert(1)">';
    +
    +      Handlebars.registerPartial('testPartial', '{{> @partial-block}}');
    +      try {
    +        expect(function() {
    +          Handlebars.compile('{{> testPartial}}')({});
    +        }).to.throw(/could not be found/);
    +      } finally {
    +        Handlebars.unregisterPartial('testPartial');
    +      }
    +    });
    +
    +    it('should still render legitimate @partial-block content', function() {
    +      Handlebars.registerPartial('wrapper', '<div>{{> @partial-block}}</div>');
    +      try {
    +        var result = Handlebars.compile('{{#> wrapper}}hello{{/wrapper}}')({});
    +        expect(result).to.equal('<div>hello</div>');
    +      } finally {
    +        Handlebars.unregisterPartial('wrapper');
    +      }
    +    });
    +  });
     });
     
     function wrapToAdjustContainer(precompiledTemplateFunction) {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.