diff --git a/packages/pug-code-gen/index.js b/packages/pug-code-gen/index.js index 738b8f7cf..616f4cc87 100644 --- a/packages/pug-code-gen/index.js +++ b/packages/pug-code-gen/index.js @@ -335,7 +335,7 @@ Compiler.prototype = { } if (debug && node.debug !== false && node.type !== 'Block') { - if (node.line) { + if (Object.prototype.hasOwnProperty.call(node, 'line') && node.line) { var js = ';pug_debug_line = ' + node.line; if (node.filename) js += ';pug_debug_filename = ' + stringify(node.filename); @@ -414,7 +414,7 @@ Compiler.prototype = { } else { this.buf.push('case ' + node.expr + ':'); } - if (node.block) { + if (Object.prototype.hasOwnProperty.call(node, 'block') && node.block) { this.visit(node.block, node); this.buf.push(' break;'); } @@ -450,7 +450,9 @@ Compiler.prototype = { pp && block.nodes.length > 1 && !escapePrettyMode && + Object.prototype.hasOwnProperty.call(block.nodes[0], 'type') && block.nodes[0].type === 'Text' && + Object.prototype.hasOwnProperty.call(block.nodes[1], 'type') && block.nodes[1].type === 'Text' ) { this.prettyIndent(1, true); @@ -461,8 +463,11 @@ Compiler.prototype = { pp && i > 0 && !escapePrettyMode && + Object.prototype.hasOwnProperty.call(block.nodes[i], 'type') && block.nodes[i].type === 'Text' && + Object.prototype.hasOwnProperty.call(block.nodes[i - 1], 'type') && block.nodes[i - 1].type === 'Text' && + Object.prototype.hasOwnProperty.call(block.nodes[i - 1], 'val') && /\n$/.test(block.nodes[i - 1].val) ) { this.prettyIndent(1, false); @@ -671,11 +676,17 @@ Compiler.prototype = { } // if it is non-empty throw an error if ( - tag.code || - (tag.block && + (Object.prototype.hasOwnProperty.call(tag, 'code') && tag.code) || + (Object.prototype.hasOwnProperty.call(tag, 'block') && + tag.block && !(tag.block.type === 'Block' && tag.block.nodes.length === 0) && tag.block.nodes.some(function(tag) { - return tag.type !== 'Text' || !/^\s*$/.test(tag.val); + return ( + !Object.prototype.hasOwnProperty.call(tag, 'type') || + tag.type !== 'Text' || + !Object.prototype.hasOwnProperty.call(tag, 'val') || + !/^\s*$/.test(tag.val) + ); })) ) { this.error( @@ -696,7 +707,7 @@ Compiler.prototype = { this.attributeBlocks(tag.attributeBlocks) ); this.buffer('>'); - if (tag.code) this.visitCode(tag.code); + if (Object.prototype.hasOwnProperty.call(tag, 'code') && tag.code) this.visitCode(tag.code); this.visit(tag.block, tag); // pretty print @@ -807,7 +818,7 @@ Compiler.prototype = { } // Block support - if (code.block) { + if (Object.prototype.hasOwnProperty.call(code, 'block') && code.block) { if (!code.buffer) this.buf.push('{'); this.visit(code.block, code); if (!code.buffer) this.buf.push('}'); @@ -826,7 +837,7 @@ Compiler.prototype = { this.buf.push('if (' + test + ') {'); this.visit(cond.consequent, cond); this.buf.push('}'); - if (cond.alternate) { + if (Object.prototype.hasOwnProperty.call(cond, 'alternate') && cond.alternate) { if (cond.alternate.type === 'Conditional') { this.buf.push('else'); this.visitConditional(cond.alternate); @@ -1021,12 +1032,16 @@ Compiler.prototype = { function tagCanInline(tag) { function isInline(node) { // Recurse if the node is a block - if (node.type === 'Block') return node.nodes.every(isInline); + if (Object.prototype.hasOwnProperty.call(node, 'type') && node.type === 'Block') return node.nodes.every(isInline); // When there is a YieldBlock here, it is an indication that the file is // expected to be included but is not. If this is the case, the block // must be empty. - if (node.type === 'YieldBlock') return true; - return (node.type === 'Text' && !/\n/.test(node.val)) || node.isInline; + if (Object.prototype.hasOwnProperty.call(node, 'type') && node.type === 'YieldBlock') return true; + return ( + (Object.prototype.hasOwnProperty.call(node, 'type') && node.type === 'Text' && + Object.prototype.hasOwnProperty.call(node, 'val') && !/\n/.test(node.val)) || + (Object.prototype.hasOwnProperty.call(node, 'isInline') && node.isInline) + ); } return tag.block.nodes.every(isInline); diff --git a/packages/pug-filters/lib/handle-filters.js b/packages/pug-filters/lib/handle-filters.js index 884a50138..080b0ae3f 100644 --- a/packages/pug-filters/lib/handle-filters.js +++ b/packages/pug-filters/lib/handle-filters.js @@ -6,6 +6,8 @@ var walk = require('pug-walk'); var error = require('pug-error'); var runFilter = require('./run-filter'); +var ownProp = Object.prototype.hasOwnProperty; + module.exports = handleFilters; function handleFilters(ast, filters, options, filterAliases) { options = options || {}; @@ -13,14 +15,19 @@ function handleFilters(ast, filters, options, filterAliases) { ast, function(node) { var dir = node.filename ? dirname(node.filename) : null; - if (node.type === 'Filter') { + if (ownProp.call(node, 'type') && node.type === 'Filter') { handleNestedFilters(node, filters, options, filterAliases); var text = getBodyAsText(node); var attrs = getAttributes(node, options); attrs.filename = node.filename; node.type = 'Text'; node.val = filterWithFallback(node, text, attrs); - } else if (node.type === 'RawInclude' && node.filters.length) { + } else if ( + ownProp.call(node, 'type') && + node.type === 'RawInclude' && + ownProp.call(node, 'filters') && + node.filters.length + ) { var firstFilter = node.filters.pop(); var attrs = getAttributes(firstFilter, options); var filename = (attrs.filename = node.file.fullPath); @@ -98,7 +105,12 @@ function handleFilters(ast, filters, options, filterAliases) { } function handleNestedFilters(node, filters, options, filterAliases) { - if (node.block.nodes[0] && node.block.nodes[0].type === 'Filter') { + if ( + ownProp.call(node, 'block') && + node.block.nodes[0] && + ownProp.call(node.block.nodes[0], 'type') && + node.block.nodes[0].type === 'Filter' + ) { node.block.nodes[0] = handleFilters( node.block, filters, diff --git a/packages/pug-linker/index.js b/packages/pug-linker/index.js index 256d74bb0..0b8a24091 100644 --- a/packages/pug-linker/index.js +++ b/packages/pug-linker/index.js @@ -3,6 +3,8 @@ var assert = require('assert'); var walk = require('pug-walk'); +var ownProp = Object.prototype.hasOwnProperty; + function error() { throw require('pug-error').apply(null, arguments); } @@ -10,12 +12,13 @@ function error() { module.exports = link; function link(ast) { assert( - ast.type === 'Block', + ownProp.call(ast, 'type') && ast.type === 'Block', 'The top level element should always be a block' ); var extendsNode = null; if (ast.nodes.length) { - var hasExtends = ast.nodes[0].type === 'Extends'; + var hasExtends = + ownProp.call(ast.nodes[0], 'type') && ast.nodes[0].type === 'Extends'; checkExtendPosition(ast, hasExtends); if (hasExtends) { extendsNode = ast.nodes.shift(); @@ -27,11 +30,16 @@ function link(ast) { var mixins = []; var expectedBlocks = []; ast.nodes.forEach(function addNode(node) { - if (node.type === 'NamedBlock') { + if (ownProp.call(node, 'type') && node.type === 'NamedBlock') { expectedBlocks.push(node); - } else if (node.type === 'Block') { + } else if (ownProp.call(node, 'type') && node.type === 'Block') { node.nodes.forEach(addNode); - } else if (node.type === 'Mixin' && node.call === false) { + } else if ( + ownProp.call(node, 'type') && + node.type === 'Mixin' && + ownProp.call(node, 'call') && + node.call === false + ) { mixins.push(node); } else { error( @@ -45,7 +53,7 @@ function link(ast) { extend(parent.declaredBlocks, ast); var foundBlockNames = []; walk(parent, function(node) { - if (node.type === 'NamedBlock') { + if (ownProp.call(node, 'type') && node.type === 'NamedBlock') { foundBlockNames.push(node.name); } }); @@ -71,7 +79,12 @@ function link(ast) { function findDeclaredBlocks(ast) /*: {[name: string]: Array}*/ { var definitions = {}; walk(ast, function before(node) { - if (node.type === 'NamedBlock' && node.mode === 'replace') { + if ( + ownProp.call(node, 'type') && + node.type === 'NamedBlock' && + ownProp.call(node, 'mode') && + node.mode === 'replace' + ) { definitions[node.name] = definitions[node.name] || []; definitions[node.name].push(node); } @@ -82,7 +95,7 @@ function findDeclaredBlocks(ast) /*: {[name: string]: Array}*/ { function flattenParentBlocks(parentBlocks, accumulator) { accumulator = accumulator || []; parentBlocks.forEach(function(parentBlock) { - if (parentBlock.parents) { + if (ownProp.call(parentBlock, 'parents') && parentBlock.parents) { flattenParentBlocks(parentBlock.parents, accumulator); } accumulator.push(parentBlock); @@ -95,7 +108,7 @@ function extend(parentBlocks, ast) { walk( ast, function before(node) { - if (node.type === 'NamedBlock') { + if (ownProp.call(node, 'type') && node.type === 'NamedBlock') { if (stack[node.name] === node.name) { return (node.ignore = true); } @@ -106,7 +119,7 @@ function extend(parentBlocks, ast) { if (parentBlockList.length) { node.parents = parentBlockList; parentBlockList.forEach(function(parentBlock) { - switch (node.mode) { + switch (ownProp.call(node, 'mode') ? node.mode : undefined) { case 'append': parentBlock.nodes = parentBlock.nodes.concat(node.nodes); break; @@ -122,7 +135,11 @@ function extend(parentBlocks, ast) { } }, function after(node) { - if (node.type === 'NamedBlock' && !node.ignore) { + if ( + ownProp.call(node, 'type') && + node.type === 'NamedBlock' && + !(ownProp.call(node, 'ignore') && node.ignore) + ) { delete stack[node.name]; } } @@ -133,24 +150,24 @@ function applyIncludes(ast, child) { return walk( ast, function before(node, replace) { - if (node.type === 'RawInclude') { + if (ownProp.call(node, 'type') && node.type === 'RawInclude') { replace({type: 'Text', val: node.file.str.replace(/\r/g, '')}); } }, function after(node, replace) { - if (node.type === 'Include') { + if (ownProp.call(node, 'type') && node.type === 'Include') { var childAST = link(node.file.ast); if (childAST.hasExtends) { childAST = removeBlocks(childAST); } - replace(applyYield(childAST, node.block)); + replace(applyYield(childAST, ownProp.call(node, 'block') ? node.block : null)); } } ); } function removeBlocks(ast) { return walk(ast, function(node, replace) { - if (node.type === 'NamedBlock') { + if (ownProp.call(node, 'type') && node.type === 'NamedBlock') { replace({ type: 'Block', nodes: node.nodes, @@ -163,7 +180,7 @@ function applyYield(ast, block) { if (!block || !block.nodes.length) return ast; var replaced = false; ast = walk(ast, null, function(node, replace) { - if (node.type === 'YieldBlock') { + if (ownProp.call(node, 'type') && node.type === 'YieldBlock') { replaced = true; node.type = 'Block'; node.nodes = [block]; @@ -172,10 +189,14 @@ function applyYield(ast, block) { function defaultYieldLocation(node) { var res = node; for (var i = 0; i < node.nodes.length; i++) { - if (node.nodes[i].textOnly) continue; - if (node.nodes[i].type === 'Block') { + if (ownProp.call(node.nodes[i], 'textOnly') && node.nodes[i].textOnly) continue; + if (ownProp.call(node.nodes[i], 'type') && node.nodes[i].type === 'Block') { res = defaultYieldLocation(node.nodes[i]); - } else if (node.nodes[i].block && node.nodes[i].block.nodes.length) { + } else if ( + ownProp.call(node.nodes[i], 'block') && + node.nodes[i].block && + node.nodes[i].block.nodes.length + ) { res = defaultYieldLocation(node.nodes[i].block); } } @@ -191,7 +212,7 @@ function applyYield(ast, block) { function checkExtendPosition(ast, hasExtends) { var legitExtendsReached = false; walk(ast, function(node) { - if (node.type === 'Extends') { + if (ownProp.call(node, 'type') && node.type === 'Extends') { if (hasExtends && !legitExtendsReached) { legitExtendsReached = true; } else { diff --git a/packages/pug-walk/index.js b/packages/pug-walk/index.js index 9972e4b2f..c7b12282c 100644 --- a/packages/pug-walk/index.js +++ b/packages/pug-walk/index.js @@ -1,5 +1,7 @@ 'use strict'; +var ownProp = Object.prototype.hasOwnProperty; + module.exports = walkAST; function walkAST(ast, before, after, options) { if (after && typeof after === 'object' && typeof options === 'undefined') { @@ -34,7 +36,12 @@ function walkAST(ast, before, after, options) { parents.unshift(ast); - switch (ast.type) { + // Use own-property check to prevent prototype pollution gadgets: + // an attacker could set Object.prototype.type to a node type string, + // causing arbitrary AST branches to be walked on plain objects. + var astType = ownProp.call(ast, 'type') ? ast.type : undefined; + + switch (astType) { case 'NamedBlock': case 'Block': ast.nodes = walkAndMergeNodes(ast.nodes); @@ -47,28 +54,28 @@ function walkAST(ast, before, after, options) { case 'When': case 'Code': case 'While': - if (ast.block) { + if (ownProp.call(ast, 'block') && ast.block) { ast.block = walkAST(ast.block, before, after, options); } break; case 'Each': - if (ast.block) { + if (ownProp.call(ast, 'block') && ast.block) { ast.block = walkAST(ast.block, before, after, options); } - if (ast.alternate) { + if (ownProp.call(ast, 'alternate') && ast.alternate) { ast.alternate = walkAST(ast.alternate, before, after, options); } break; case 'EachOf': - if (ast.block) { + if (ownProp.call(ast, 'block') && ast.block) { ast.block = walkAST(ast.block, before, after, options); } break; case 'Conditional': - if (ast.consequent) { + if (ownProp.call(ast, 'consequent') && ast.consequent) { ast.consequent = walkAST(ast.consequent, before, after, options); } - if (ast.alternate) { + if (ownProp.call(ast, 'alternate') && ast.alternate) { ast.alternate = walkAST(ast.alternate, before, after, options); } break; @@ -93,7 +100,7 @@ function walkAST(ast, before, after, options) { case 'Text': break; case 'FileReference': - if (options.includeDependencies && ast.ast) { + if (options.includeDependencies && ownProp.call(ast, 'ast') && ast.ast) { walkAST(ast.ast, before, after, options); } break;