Fix security issues
Fixes GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r, GHSA-2qvq-rjwj-gvw9, GHSA-9cx6-37pm-9jff, GHSA-7rx3-28cr-v5wh, GHSA-442j-39wm-28r2, GHSA-xjpj-3mr7-gcpf
This commit is contained in:
committed by
Jay Linski
parent
b2a083136b
commit
68d8df5a88
@@ -1,6 +1,7 @@
|
|||||||
import parser from './parser';
|
import parser from './parser';
|
||||||
import WhitespaceControl from './whitespace-control';
|
import WhitespaceControl from './whitespace-control';
|
||||||
import * as Helpers from './helpers';
|
import * as Helpers from './helpers';
|
||||||
|
import Exception from '../exception';
|
||||||
import { extend } from '../utils';
|
import { extend } from '../utils';
|
||||||
|
|
||||||
export { parser };
|
export { parser };
|
||||||
@@ -11,6 +12,9 @@ extend(yy, Helpers);
|
|||||||
export function parseWithoutProcessing(input, options) {
|
export function parseWithoutProcessing(input, options) {
|
||||||
// Just return if an already-compiled AST was passed in.
|
// Just return if an already-compiled AST was passed in.
|
||||||
if (input.type === 'Program') {
|
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;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,3 +36,66 @@ export function parse(input, options) {
|
|||||||
|
|
||||||
return strip.accept(ast);
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -686,9 +686,18 @@ JavaScriptCompiler.prototype = {
|
|||||||
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
|
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
|
||||||
options = this.setupHelperArgs(name, paramSize);
|
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([
|
this.decorators.push([
|
||||||
'fn = ',
|
'fn = ',
|
||||||
this.decorators.functionCall(foundDecorator, '', [
|
this.decorators.functionCall('decorator', '', [
|
||||||
'fn',
|
'fn',
|
||||||
'props',
|
'props',
|
||||||
'container',
|
'container',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function createProtoAccessControl(runtimeOptions) {
|
|||||||
methodWhiteList['__defineGetter__'] = false;
|
methodWhiteList['__defineGetter__'] = false;
|
||||||
methodWhiteList['__defineSetter__'] = false;
|
methodWhiteList['__defineSetter__'] = false;
|
||||||
methodWhiteList['__lookupGetter__'] = false;
|
methodWhiteList['__lookupGetter__'] = false;
|
||||||
|
methodWhiteList['__lookupSetter__'] = false;
|
||||||
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
|
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export function template(templateSpec, env) {
|
|||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
let result = depths[i] && container.lookupProperty(depths[i], name);
|
let result = depths[i] && container.lookupProperty(depths[i], name);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return depths[i][name];
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -349,21 +349,21 @@ export function wrapProgram(
|
|||||||
export function resolvePartial(partial, context, options) {
|
export function resolvePartial(partial, context, options) {
|
||||||
if (!partial) {
|
if (!partial) {
|
||||||
if (options.name === '@partial-block') {
|
if (options.name === '@partial-block') {
|
||||||
partial = options.data['partial-block'];
|
partial = lookupOwnProperty(options.data, 'partial-block');
|
||||||
} else {
|
} else {
|
||||||
partial = options.partials[options.name];
|
partial = lookupOwnProperty(options.partials, options.name);
|
||||||
}
|
}
|
||||||
} else if (!partial.call && !options.name) {
|
} else if (!partial.call && !options.name) {
|
||||||
// This is a dynamic partial that returned a string
|
// This is a dynamic partial that returned a string
|
||||||
options.name = partial;
|
options.name = partial;
|
||||||
partial = options.partials[partial];
|
partial = lookupOwnProperty(options.partials, partial);
|
||||||
}
|
}
|
||||||
return partial;
|
return partial;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invokePartial(partial, context, options) {
|
export function invokePartial(partial, context, options) {
|
||||||
// Use the current closure context to save the partial-block if this partial
|
// 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;
|
options.partial = true;
|
||||||
if (options.ids) {
|
if (options.ids) {
|
||||||
options.data.contextPath = options.ids[0] || options.data.contextPath;
|
options.data.contextPath = options.ids[0] || options.data.contextPath;
|
||||||
@@ -404,6 +404,12 @@ export function noop() {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lookupOwnProperty(obj, name) {
|
||||||
|
if (obj && Object.prototype.hasOwnProperty.call(obj, name)) {
|
||||||
|
return obj[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initData(context, data) {
|
function initData(context, data) {
|
||||||
if (!data || !('root' in data)) {
|
if (!data || !('root' in data)) {
|
||||||
data = data ? createFrame(data) : {};
|
data = data ? createFrame(data) : {};
|
||||||
|
|||||||
+45
-8
@@ -196,16 +196,24 @@ module.exports.cli = function(opts) {
|
|||||||
|
|
||||||
const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
|
const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
|
||||||
|
|
||||||
|
if (opts.namespace && !isValidNamespace(opts.namespace)) {
|
||||||
|
throw new Handlebars.Exception('Invalid namespace format');
|
||||||
|
}
|
||||||
|
|
||||||
let output = new SourceNode();
|
let output = new SourceNode();
|
||||||
if (!opts.simple) {
|
if (!opts.simple) {
|
||||||
if (opts.amd) {
|
if (opts.amd) {
|
||||||
|
const runtimeModulePath =
|
||||||
|
(opts.handlebarPath || '') + 'handlebars.runtime';
|
||||||
output.add(
|
output.add(
|
||||||
"define(['" +
|
'define([' +
|
||||||
opts.handlebarPath +
|
quoteForJavaScript(runtimeModulePath) +
|
||||||
'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
|
'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
|
||||||
);
|
);
|
||||||
} else if (opts.commonjs) {
|
} else if (opts.commonjs) {
|
||||||
output.add('var Handlebars = require("' + opts.commonjs + '");');
|
output.add(
|
||||||
|
'var Handlebars = require(' + quoteForJavaScript(opts.commonjs) + ');'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
output.add('(function() {\n');
|
output.add('(function() {\n');
|
||||||
}
|
}
|
||||||
@@ -255,9 +263,9 @@ module.exports.cli = function(opts) {
|
|||||||
}
|
}
|
||||||
output.add([
|
output.add([
|
||||||
objectName,
|
objectName,
|
||||||
"['",
|
'[',
|
||||||
template.name,
|
quoteForJavaScript(template.name),
|
||||||
"'] = template(",
|
'] = template(',
|
||||||
precompiled,
|
precompiled,
|
||||||
');\n'
|
');\n'
|
||||||
]);
|
]);
|
||||||
@@ -277,7 +285,9 @@ module.exports.cli = function(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts.map) {
|
if (opts.map) {
|
||||||
output.add('\n//# sourceMappingURL=' + opts.map + '\n');
|
output.add(
|
||||||
|
'\n//# sourceMappingURL=' + sanitizeSourceMapComment(opts.map) + '\n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
output = output.toStringWithSourceMap();
|
output = output.toStringWithSourceMap();
|
||||||
@@ -307,6 +317,33 @@ function arrayCast(value) {
|
|||||||
return 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.
|
* Run uglify to minify the compiled template, if uglify exists in the dependencies.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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() {
|
it('can pass through an empty string', function() {
|
||||||
equal(Handlebars.compile('')(), '');
|
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 || {};
|
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";
|
return "a";
|
||||||
},"useData":true});
|
},"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 || {};
|
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 "";
|
return "";
|
||||||
},"useData":true});
|
},"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 || {};
|
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 "";
|
return "";
|
||||||
},"useData":true});
|
},"useData":true});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
|
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 "";
|
return "";
|
||||||
},"useData":true});
|
},"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 || {};
|
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>";
|
return "<div>1</div>";
|
||||||
},"useData":true});
|
},"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>";
|
return "<div>2</div>";
|
||||||
},"useData":true});
|
},"useData":true});
|
||||||
return templates;
|
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 || {};
|
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>";
|
return "<div>Test Partial</div>";
|
||||||
},"useData":true});
|
},"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 || {};
|
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 "";
|
return "";
|
||||||
},"useData":true});
|
},"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 || {};
|
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 "";
|
return "";
|
||||||
},"useData":true});
|
},"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 "";
|
return "";
|
||||||
},"useData":true});
|
},"useData":true});
|
||||||
return templates;
|
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 || {};
|
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>";
|
return "<div>This is a test</div>";
|
||||||
},"useData":true});
|
},"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 || {};
|
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) {
|
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
|
||||||
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
|
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
|
||||||
return parent[propertyName];
|
return parent[propertyName];
|
||||||
|
|||||||
@@ -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 || {};
|
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>";
|
return "<div>Test Partial</div>";
|
||||||
},"useData":true});
|
},"useData":true});
|
||||||
});
|
});
|
||||||
+82
-1
@@ -182,7 +182,7 @@ describe('precompiler', function() {
|
|||||||
return 'amd';
|
return 'amd';
|
||||||
};
|
};
|
||||||
Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
|
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);
|
equal(/template\(amd\)/.test(log), true);
|
||||||
});
|
});
|
||||||
it('should output multiple amd partials', function() {
|
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/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ describe('runtime', function() {
|
|||||||
/Template was precompiled with an older version of Handlebars than the current runtime/
|
/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() {
|
describe('#child', function() {
|
||||||
|
|||||||
@@ -133,11 +133,13 @@ describe('security issues', function() {
|
|||||||
'{{__defineGetter__}}',
|
'{{__defineGetter__}}',
|
||||||
'{{__defineSetter__}}',
|
'{{__defineSetter__}}',
|
||||||
'{{__lookupGetter__}}',
|
'{{__lookupGetter__}}',
|
||||||
|
'{{__lookupSetter__}}',
|
||||||
'{{__proto__}}',
|
'{{__proto__}}',
|
||||||
'{{lookup this "constructor"}}',
|
'{{lookup this "constructor"}}',
|
||||||
'{{lookup this "__defineGetter__"}}',
|
'{{lookup this "__defineGetter__"}}',
|
||||||
'{{lookup this "__defineSetter__"}}',
|
'{{lookup this "__defineSetter__"}}',
|
||||||
'{{lookup this "__lookupGetter__"}}',
|
'{{lookup this "__lookupGetter__"}}',
|
||||||
|
'{{lookup this "__lookupSetter__"}}',
|
||||||
'{{lookup this "__proto__"}}'
|
'{{lookup this "__proto__"}}'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -422,6 +424,159 @@ describe('security issues', function() {
|
|||||||
.toCompileTo('c');
|
.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) {
|
function wrapToAdjustContainer(precompiledTemplateFunction) {
|
||||||
|
|||||||
Reference in New Issue
Block a user