Compare commits
18 Commits
fix-1838-2
...
4.x
| Author | SHA1 | Date | |
|---|---|---|---|
| d069c1caf1 | |||
| 6714e07a6a | |||
| dce542c9a6 | |||
| 8a41389ba5 | |||
| 68d8df5a88 | |||
| b2a083136b | |||
| 9f98c16298 | |||
| 45443b4290 | |||
| 8841a5f6d3 | |||
| e0137c26f2 | |||
| e914d6037f | |||
| 7de4b41c34 | |||
| eab1d141cb | |||
| de4414d7fc | |||
| 08fddee033 | |||
| 4512766919 | |||
| e497a35d7f | |||
| 8c9f866655 |
@@ -12,10 +12,10 @@ jobs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
@@ -33,16 +33,16 @@ jobs:
|
||||
matrix:
|
||||
operating-system: ['ubuntu-latest', 'windows-latest']
|
||||
# https://nodejs.org/en/about/releases/
|
||||
node-version: ['10', '12', '14', '16', '18', '20']
|
||||
node-version: ['16', '18', '20', '22']
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Test (Integration)
|
||||
# https://github.com/webpack/webpack/issues/14532
|
||||
if: ${{ matrix.node-version != '18' && matrix.node-version != '20' }}
|
||||
if: ${{ matrix.node-version == '16' }}
|
||||
run: |
|
||||
cd ./tests/integration/rollup-test && ./test.sh && cd -
|
||||
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
|
||||
@@ -62,15 +62,15 @@ jobs:
|
||||
|
||||
browser:
|
||||
name: Test (Browser)
|
||||
runs-on: 'ubuntu-20.04'
|
||||
runs-on: 'ubuntu-22.04'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
|
||||
@@ -15,5 +15,6 @@ node_modules
|
||||
lib/handlebars/compiler/parser.js
|
||||
/coverage/
|
||||
/dist/
|
||||
/test-results/
|
||||
/tests/integration/*/dist/
|
||||
/spec/tmp/*
|
||||
|
||||
+8
-4
@@ -132,9 +132,11 @@ A full release via Docker may be completed with the following:
|
||||
|
||||
# Generate config for yo generator-release:
|
||||
# https://github.com/kpdecker/generator-release#example
|
||||
# You have to add a valid GitHub OAuth token!
|
||||
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub OAuth token'\n};" > /home/node/.config/generator-release
|
||||
# You have to add a valid GitHub access token! (Used for reading issues and pull requests.)
|
||||
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub personal access token'\n};" > /home/node/.config/generator-release
|
||||
RUN chown -R node:node /home/node/.config
|
||||
RUN chown -R node:node /home/node/.ssh
|
||||
RUN chown -R node:node /home/node/tmp
|
||||
|
||||
# Add the generated key to GitHub: https://github.com/settings/keys
|
||||
RUN ssh-keygen -q -t ed25519 -N '' -f /home/node/.ssh/id_ed25519 -C "release@handlebarsjs.com"
|
||||
@@ -155,9 +157,12 @@ A full release via Docker may be completed with the following:
|
||||
* Add GitHub API token: `vi /home/node/.config/generator-release`
|
||||
* Execute the following steps:
|
||||
```bash
|
||||
npm ci
|
||||
npm install
|
||||
npm install -g yo@1 grunt@1 generator-release
|
||||
npm run release
|
||||
# Warning! This step will collect data from GitHub, bump the version,
|
||||
# create a new commit, create a new tag and push it to GitHub.
|
||||
# https://github.com/kpdecker/generator-release?tab=readme-ov-file#usage
|
||||
yo release
|
||||
npm login
|
||||
npm publish
|
||||
@@ -168,7 +173,6 @@ A full release via Docker may be completed with the following:
|
||||
docker run --rm --interactive --tty \
|
||||
--volume $PWD:/app \
|
||||
--workdir /app \
|
||||
--user $(id -u):$(id -g) \
|
||||
ruby:3.2-slim bash
|
||||
```
|
||||
* Execute the following steps:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"version": "4.7.8",
|
||||
"version": "4.7.9",
|
||||
"main": "handlebars.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "components/handlebars.js",
|
||||
"description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
|
||||
"homepage": "http://handlebarsjs.com",
|
||||
"homepage": "https://handlebarsjs.com",
|
||||
"license": "MIT",
|
||||
"type": "component",
|
||||
"keywords": [
|
||||
@@ -11,13 +11,9 @@
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Wanstrath",
|
||||
"homepage": "http://chriswanstrath.com"
|
||||
"name": "Chris Wanstrath"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"robloach/component-installer": "*"
|
||||
},
|
||||
"extra": {
|
||||
"component": {
|
||||
"name": "handlebars",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<package>
|
||||
<metadata>
|
||||
<id>handlebars.js</id>
|
||||
<version>4.7.8</version>
|
||||
<version>4.7.9</version>
|
||||
<authors>handlebars.js Authors</authors>
|
||||
<licenseUrl>https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE</licenseUrl>
|
||||
<projectUrl>https://github.com/handlebars-lang/handlebars.js/</projectUrl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"version": "4.7.8",
|
||||
"version": "4.7.9",
|
||||
"license": "MIT",
|
||||
"jspm": {
|
||||
"main": "handlebars",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { registerDefaultDecorators } from './decorators';
|
||||
import logger from './logger';
|
||||
import { resetLoggedProperties } from './internal/proto-access';
|
||||
|
||||
export const VERSION = '4.7.8';
|
||||
export const VERSION = '4.7.9';
|
||||
export const COMPILER_REVISION = 8;
|
||||
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,12 +165,10 @@ JavaScriptCompiler.prototype = {
|
||||
|
||||
let { programs, decorators } = this.context;
|
||||
for (i = 0, l = programs.length; i < l; i++) {
|
||||
if (programs[i]) {
|
||||
ret[i] = programs[i];
|
||||
if (decorators[i]) {
|
||||
ret[i + '_d'] = decorators[i];
|
||||
ret.useDecorators = true;
|
||||
}
|
||||
ret[i] = programs[i];
|
||||
if (decorators[i]) {
|
||||
ret[i + '_d'] = decorators[i];
|
||||
ret.useDecorators = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,16 +533,22 @@ JavaScriptCompiler.prototype = {
|
||||
this.resolvePath('data', parts, 0, true, strict);
|
||||
},
|
||||
|
||||
resolvePath: function(type, parts, i, falsy, strict) {
|
||||
resolvePath: function(type, parts, startPartIndex, falsy, strict) {
|
||||
if (this.options.strict || this.options.assumeObjects) {
|
||||
this.push(
|
||||
strictLookup(this.options.strict && strict, this, parts, i, type)
|
||||
strictLookup(
|
||||
this.options.strict && strict,
|
||||
this,
|
||||
parts,
|
||||
startPartIndex,
|
||||
type
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let len = parts.length;
|
||||
for (; i < len; i++) {
|
||||
for (let i = startPartIndex; i < len; i++) {
|
||||
/* eslint-disable no-loop-func */
|
||||
this.replaceStack(current => {
|
||||
let lookup = this.nameLookup(current, parts[i], type);
|
||||
@@ -682,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',
|
||||
@@ -908,8 +921,8 @@ JavaScriptCompiler.prototype = {
|
||||
let existing = this.matchExistingProgram(child);
|
||||
|
||||
if (existing == null) {
|
||||
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
|
||||
let index = this.context.programs.length;
|
||||
// Placeholder to prevent name conflicts for nested children
|
||||
let index = this.context.programs.push('') - 1;
|
||||
child.index = index;
|
||||
child.name = 'program' + index;
|
||||
this.context.programs[index] = compiler.compile(
|
||||
@@ -1263,14 +1276,14 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
|
||||
);
|
||||
};
|
||||
|
||||
function strictLookup(requireTerminal, compiler, parts, i, type) {
|
||||
function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
|
||||
let stack = compiler.popStack(),
|
||||
len = parts.length;
|
||||
if (requireTerminal) {
|
||||
len--;
|
||||
}
|
||||
|
||||
for (; i < len; i++) {
|
||||
for (let i = startPartIndex; i < len; i++) {
|
||||
stack = compiler.nameLookup(stack, parts[i], type);
|
||||
}
|
||||
|
||||
@@ -1280,7 +1293,7 @@ function strictLookup(requireTerminal, compiler, parts, i, type) {
|
||||
'(',
|
||||
stack,
|
||||
', ',
|
||||
compiler.quotedString(parts[i]),
|
||||
compiler.quotedString(parts[len]),
|
||||
', ',
|
||||
JSON.stringify(compiler.source.currentLocation),
|
||||
' )'
|
||||
|
||||
@@ -20,7 +20,8 @@ export function moveHelperToHooks(instance, helperName, keepHelper) {
|
||||
if (instance.helpers[helperName]) {
|
||||
instance.hooks[helperName] = instance.helpers[helperName];
|
||||
if (!keepHelper) {
|
||||
delete instance.helpers[helperName];
|
||||
// Using delete is slow
|
||||
instance.helpers[helperName] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { extend } from '../utils';
|
||||
|
||||
/**
|
||||
* Create a new object with "null"-prototype to avoid truthy results on prototype properties.
|
||||
* The resulting object can be used with "object[property]" to check if a property exists
|
||||
* @param {...object} sources a varargs parameter of source objects that will be merged
|
||||
* @returns {object}
|
||||
*/
|
||||
export function createNewLookupObject(...sources) {
|
||||
return extend(Object.create(null), ...sources);
|
||||
}
|
||||
@@ -1,32 +1,31 @@
|
||||
import { createNewLookupObject } from './create-new-lookup-object';
|
||||
import { extend } from '../utils';
|
||||
import logger from '../logger';
|
||||
|
||||
const loggedProperties = Object.create(null);
|
||||
|
||||
export function createProtoAccessControl(runtimeOptions) {
|
||||
let defaultMethodWhiteList = Object.create(null);
|
||||
defaultMethodWhiteList['constructor'] = false;
|
||||
defaultMethodWhiteList['__defineGetter__'] = false;
|
||||
defaultMethodWhiteList['__defineSetter__'] = false;
|
||||
defaultMethodWhiteList['__lookupGetter__'] = false;
|
||||
|
||||
let defaultPropertyWhiteList = Object.create(null);
|
||||
// Create an object with "null"-prototype to avoid truthy results on
|
||||
// prototype properties.
|
||||
const propertyWhiteList = Object.create(null);
|
||||
// eslint-disable-next-line no-proto
|
||||
defaultPropertyWhiteList['__proto__'] = false;
|
||||
propertyWhiteList['__proto__'] = false;
|
||||
extend(propertyWhiteList, runtimeOptions.allowedProtoProperties);
|
||||
|
||||
const methodWhiteList = Object.create(null);
|
||||
methodWhiteList['constructor'] = false;
|
||||
methodWhiteList['__defineGetter__'] = false;
|
||||
methodWhiteList['__defineSetter__'] = false;
|
||||
methodWhiteList['__lookupGetter__'] = false;
|
||||
methodWhiteList['__lookupSetter__'] = false;
|
||||
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
|
||||
|
||||
return {
|
||||
properties: {
|
||||
whitelist: createNewLookupObject(
|
||||
defaultPropertyWhiteList,
|
||||
runtimeOptions.allowedProtoProperties
|
||||
),
|
||||
whitelist: propertyWhiteList,
|
||||
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
|
||||
},
|
||||
methods: {
|
||||
whitelist: createNewLookupObject(
|
||||
defaultMethodWhiteList,
|
||||
runtimeOptions.allowedProtoMethods
|
||||
),
|
||||
whitelist: methodWhiteList,
|
||||
defaultValue: runtimeOptions.allowProtoMethodsByDefault
|
||||
}
|
||||
};
|
||||
|
||||
+24
-22
@@ -74,17 +74,10 @@ export function template(templateSpec, env) {
|
||||
}
|
||||
partial = env.VM.resolvePartial.call(this, partial, context, options);
|
||||
|
||||
let extendedOptions = Utils.extend({}, options, {
|
||||
hooks: this.hooks,
|
||||
protoAccessControl: this.protoAccessControl
|
||||
});
|
||||
options.hooks = this.hooks;
|
||||
options.protoAccessControl = this.protoAccessControl;
|
||||
|
||||
let result = env.VM.invokePartial.call(
|
||||
this,
|
||||
partial,
|
||||
context,
|
||||
extendedOptions
|
||||
);
|
||||
let result = env.VM.invokePartial.call(this, partial, context, options);
|
||||
|
||||
if (result == null && env.compile) {
|
||||
options.partials[options.name] = env.compile(
|
||||
@@ -92,7 +85,7 @@ export function template(templateSpec, env) {
|
||||
templateSpec.compilerOptions,
|
||||
env
|
||||
);
|
||||
result = options.partials[options.name](context, extendedOptions);
|
||||
result = options.partials[options.name](context, options);
|
||||
}
|
||||
if (result != null) {
|
||||
if (options.indent) {
|
||||
@@ -145,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;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -254,8 +247,9 @@ export function template(templateSpec, env) {
|
||||
|
||||
ret._setup = function(options) {
|
||||
if (!options.partial) {
|
||||
let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
|
||||
wrapHelpersToPassLookupProperty(mergedHelpers, container);
|
||||
let mergedHelpers = {};
|
||||
addHelpers(mergedHelpers, env.helpers, container);
|
||||
addHelpers(mergedHelpers, options.helpers, container);
|
||||
container.helpers = mergedHelpers;
|
||||
|
||||
if (templateSpec.usePartial) {
|
||||
@@ -355,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;
|
||||
@@ -410,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) : {};
|
||||
@@ -435,9 +435,10 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
|
||||
return prog;
|
||||
}
|
||||
|
||||
function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
|
||||
Object.keys(mergedHelpers).forEach(helperName => {
|
||||
let helper = mergedHelpers[helperName];
|
||||
function addHelpers(mergedHelpers, helpers, container) {
|
||||
if (!helpers) return;
|
||||
Object.keys(helpers).forEach(helperName => {
|
||||
let helper = helpers[helperName];
|
||||
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
|
||||
});
|
||||
}
|
||||
@@ -445,6 +446,7 @@ function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
|
||||
function passLookupPropertyOption(helper, container) {
|
||||
const lookupProperty = container.lookupProperty;
|
||||
return wrapHelper(helper, options => {
|
||||
return Utils.extend({ lookupProperty }, options);
|
||||
options.lookupProperty = lookupProperty;
|
||||
return options;
|
||||
});
|
||||
}
|
||||
|
||||
+45
-8
@@ -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.
|
||||
*
|
||||
|
||||
Generated
+5739
-13846
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"barename": "handlebars",
|
||||
"version": "4.7.8",
|
||||
"version": "4.7.9",
|
||||
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
|
||||
"homepage": "https://handlebarsjs.com/",
|
||||
"keywords": [
|
||||
@@ -30,7 +30,7 @@
|
||||
"uglify-js": "^3.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.17.1",
|
||||
"@playwright/test": "1.44.1",
|
||||
"aws-sdk": "^2.1.49",
|
||||
"babel-loader": "^5.0.0",
|
||||
"babel-runtime": "^5.1.10",
|
||||
@@ -39,7 +39,6 @@
|
||||
"chai-diff": "^1.0.1",
|
||||
"concurrently": "^5.0.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"dtslint": "^0.5.5",
|
||||
"dustjs-linkedin": "^2.0.2",
|
||||
"eco": "~1.1.0-rc-3",
|
||||
"eslint": "^6.7.2",
|
||||
@@ -47,7 +46,7 @@
|
||||
"eslint-plugin-compat": "^3.13.0",
|
||||
"eslint-plugin-es5": "^1.4.1",
|
||||
"fs-extra": "^8.1.0",
|
||||
"grunt": "^1.0.4",
|
||||
"grunt": "1.5.3",
|
||||
"grunt-babel": "^5.0.0",
|
||||
"grunt-cli": "^1",
|
||||
"grunt-contrib-clean": "^1",
|
||||
@@ -87,7 +86,7 @@
|
||||
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
|
||||
"lint:eslint": "eslint --max-warnings 0 .",
|
||||
"lint:prettier": "prettier --check '**/*.js'",
|
||||
"lint:types": "dtslint types",
|
||||
"lint:types": "tsc --noEmit --project types",
|
||||
"test": "npm run test:mocha",
|
||||
"test:mocha": "grunt build && grunt test",
|
||||
"test:browser": "playwright test --config tests/browser/playwright.config.js tests/browser/spec.js",
|
||||
|
||||
+11
-1
@@ -2,7 +2,17 @@
|
||||
|
||||
## Development
|
||||
|
||||
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...master)
|
||||
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.9...master)
|
||||
|
||||
## v4.7.9 - March 26th, 2026
|
||||
- fix: enable shell mode for spawn to resolve Windows EINVAL issue - e0137c2
|
||||
- fix type "RuntimeOptions" also accepting string partials - eab1d14
|
||||
- feat(types): set `hash` to be a `Record<string, any>` - de4414d
|
||||
- fix non-contiguous program indices - 4512766
|
||||
- refactor: rename i to startPartIndex - e497a35
|
||||
- security: fix security issues - 68d8df5
|
||||
|
||||
[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)
|
||||
|
||||
## v4.7.8 - July 27th, 2023
|
||||
|
||||
|
||||
@@ -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('')(), '');
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
})();
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
@@ -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});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
@@ -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({"1":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];
|
||||
@@ -8,8 +8,8 @@ return parent[propertyName];
|
||||
return undefined
|
||||
};
|
||||
return " <div>Some known helper</div>\n"
|
||||
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
|
||||
},"2":function(container,depth0,helpers,partials,data) {
|
||||
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
|
||||
},"1":function(container,depth0,helpers,partials,data) {
|
||||
return " <div>Another known helper</div>\n";
|
||||
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
|
||||
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
|
||||
@@ -18,7 +18,7 @@ return parent[propertyName];
|
||||
}
|
||||
return undefined
|
||||
};
|
||||
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
|
||||
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(0, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
|
||||
},"useData":true});
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
+82
-1
@@ -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/
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-3
@@ -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() {
|
||||
@@ -91,13 +98,13 @@ describe('runtime', function() {
|
||||
it('should expose child template', function() {
|
||||
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
|
||||
// Calling twice to hit the non-compiled case.
|
||||
equal(template._child(1)(), 'bar');
|
||||
equal(template._child(1)(), 'bar');
|
||||
equal(template._child(0)(), 'bar');
|
||||
equal(template._child(0)(), 'bar');
|
||||
});
|
||||
it('should render depthed content', function() {
|
||||
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
|
||||
// Calling twice to hit the non-compiled case.
|
||||
equal(template._child(1, undefined, [], [{ bar: 'baz' }])(), 'baz');
|
||||
equal(template._child(0, undefined, [], [{ bar: 'baz' }])(), 'baz');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -23,7 +23,8 @@ async function execFileWithInheritedOutput(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const resolvedCommand = preferLocalDependencies(command);
|
||||
const child = childProcess.spawn(resolvedCommand, args, {
|
||||
stdio: 'inherit'
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32' // Workaround for CVE-2024-27980
|
||||
});
|
||||
child.on('exit', code => {
|
||||
if (code !== 0) {
|
||||
|
||||
@@ -9,6 +9,5 @@ Execute the following commands in the project root:
|
||||
```bash
|
||||
npm install
|
||||
npx grunt prepare
|
||||
docker pull mcr.microsoft.com/playwright:focal
|
||||
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:focal npm run test:browser
|
||||
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:v1.44.1-jammy npm run test:browser
|
||||
```
|
||||
@@ -2,6 +2,7 @@ const { devices } = require('@playwright/test');
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
testMatch: ['spec.js'],
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
|
||||
+10
-10
@@ -7,27 +7,27 @@ async function waitForMochaAndAssertResult(page) {
|
||||
expect(mochaResults.failures).toBe(0);
|
||||
}
|
||||
|
||||
test('Spec handlebars.js', async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/spec/?headless=true`);
|
||||
test('Spec handlebars.js', async ({ page }) => {
|
||||
await page.goto(`/spec/?headless=true`);
|
||||
await waitForMochaAndAssertResult(page);
|
||||
});
|
||||
|
||||
test('Spec handlebars.amd.js (AMD)', async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/spec/amd.html?headless=true`);
|
||||
test('Spec handlebars.amd.js (AMD)', async ({ page }) => {
|
||||
await page.goto(`/spec/amd.html?headless=true`);
|
||||
await waitForMochaAndAssertResult(page);
|
||||
});
|
||||
|
||||
test('Spec handlebars.runtime.amd.js (AMD)', async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/spec/amd-runtime.html?headless=true`);
|
||||
test('Spec handlebars.runtime.amd.js (AMD)', async ({ page }) => {
|
||||
await page.goto(`/spec/amd-runtime.html?headless=true`);
|
||||
await waitForMochaAndAssertResult(page);
|
||||
});
|
||||
|
||||
test('Spec handlebars.js (UMD)', async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/spec/umd.html?headless=true`);
|
||||
test('Spec handlebars.js (UMD)', async ({ page }) => {
|
||||
await page.goto(`/spec/umd.html?headless=true`);
|
||||
await waitForMochaAndAssertResult(page);
|
||||
});
|
||||
|
||||
test('Spec handlebars.runtime.js (UMD)', async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/spec/umd-runtime.html?headless=true`);
|
||||
test('Spec handlebars.runtime.js (UMD)', async ({ page }) => {
|
||||
await page.goto(`/spec/umd-runtime.html?headless=true`);
|
||||
await waitForMochaAndAssertResult(page);
|
||||
});
|
||||
|
||||
Vendored
+3
-3
@@ -25,7 +25,7 @@ declare namespace Handlebars {
|
||||
partial?: boolean;
|
||||
depths?: any[];
|
||||
helpers?: { [name: string]: Function };
|
||||
partials?: { [name: string]: HandlebarsTemplateDelegate };
|
||||
partials?: { [name: string]: Template };
|
||||
decorators?: { [name: string]: Function };
|
||||
data?: any;
|
||||
blockParams?: any[];
|
||||
@@ -39,7 +39,7 @@ declare namespace Handlebars {
|
||||
export interface HelperOptions {
|
||||
fn: TemplateDelegate;
|
||||
inverse: TemplateDelegate;
|
||||
hash: any;
|
||||
hash: Record<string, any>;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ declare namespace Handlebars {
|
||||
export function unregisterHelper(name: string): void;
|
||||
|
||||
export function registerPartial(name: string, fn: Template): void;
|
||||
export function registerPartial(spec: { [name: string]: HandlebarsTemplateDelegate }): void;
|
||||
export function registerPartial(spec: { [name: string]: Template }): void;
|
||||
export function unregisterPartial(name: string): void;
|
||||
|
||||
// TODO: replace Function with actual signature
|
||||
|
||||
@@ -249,6 +249,9 @@ function testProtoAccessControlControlOptions() {
|
||||
allowedProtoProperties: { allowedProperty: true, forbiddenProperty: false },
|
||||
allowProtoMethodsByDefault: true,
|
||||
allowProtoPropertiesByDefault: false,
|
||||
partials: {
|
||||
link: '<a href="/people/{{id}}">{{name}}</a>'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user