Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/assert-depth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';
module.exports = ast => {
const pending = [[ast, 0, false]];
const seen = new WeakSet();
while (pending.length) {
const [node, depth, exit] = pending.pop();
if (exit) { seen.delete(node); continue; }
if (depth > 128) throw new SyntaxError('Brace AST exceeds safe depth of 128');
if (seen.has(node)) throw new SyntaxError('Brace AST must be an acyclic tree');
seen.add(node);
pending.push([node, depth, true]);
if (node.nodes) for (const child of node.nodes) pending.push([child, depth + 1, false]);
}
};
1 change: 1 addition & 0 deletions lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fill = require('fill-range');
const utils = require('./utils');

const compile = (ast, options = {}) => {
require('./assert-depth')(ast);
const walk = (node, parent = {}) => {
const invalidBlock = utils.isInvalidBrace(parent);
const invalidNode = node.invalid === true && options.escapeInvalid === true;
Expand Down
1 change: 1 addition & 0 deletions lib/expand.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const append = (queue = '', stash = '', enclose = false) => {
};

const expand = (ast, options = {}) => {
require('./assert-depth')(ast);
const rangeLimit = options.rangeLimit === undefined ? 1000 : options.rangeLimit;

const walk = (node, parent = {}) => {
Expand Down
6 changes: 6 additions & 0 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ const parse = (input, options = {}) => {
*/

if (value === CHAR_LEFT_PARENTHESES) {
if (stack.length > 128) {
throw new SyntaxError('Brace AST exceeds safe depth of 128');
}
block = push({ type: 'paren', nodes: [] });
stack.push(block);
push({ type: 'text', value });
Expand Down Expand Up @@ -203,6 +206,9 @@ const parse = (input, options = {}) => {
*/

if (value === CHAR_LEFT_CURLY_BRACE) {
if (stack.length > 128) {
throw new SyntaxError('Brace AST exceeds safe depth of 128');
}
depth++;

const dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true;
Expand Down
1 change: 1 addition & 0 deletions lib/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const utils = require('./utils');

module.exports = (ast, options = {}) => {
require('./assert-depth')(ast);
const stringify = (node, parent = {}) => {
const invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent);
const invalidNode = node.invalid === true && options.escapeInvalid === true;
Expand Down
28 changes: 28 additions & 0 deletions test/security-depth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
require('mocha');
const assert = require('assert');
const braces = require('..');

describe('bounded parser and supplied AST traversal', () => {
for (const operation of ['compile', 'expand', 'stringify']) {
it(`rejects deep and cyclic ASTs in ${operation}`, () => {
let ast = { type: 'text', value: 'x' };
for (let i = 0; i < 12000; i++) ast = { type: 'root', nodes: [ast] };
assert.throws(() => braces[operation](ast), SyntaxError);
const cycle = { type: 'root', nodes: [] };
cycle.nodes.push(cycle);
assert.throws(() => braces[operation](cycle), SyntaxError);
});
}
it('rejects deeply nested parsed patterns', () => {
for (const pair of [['{', '}'], ['(', ')']]) {
assert.throws(() => braces.parse(pair[0].repeat(4000) + 'x' + pair[1].repeat(4000)), SyntaxError);
}
});
it('preserves ordinary ranges, nesting and shared acyclic nodes', () => {
assert.deepStrictEqual(braces.expand('a/{1..3}/{x,y}'), ['a/1/x', 'a/1/y', 'a/2/x', 'a/2/y', 'a/3/x', 'a/3/y']);
const leaf = { type: 'text', value: 'a' };
assert.strictEqual(braces.stringify({ type: 'root', nodes: [leaf, leaf] }), 'aa');
assert.doesNotThrow(() => braces.compile('{'.repeat(64) + 'x,y' + '}'.repeat(64)));
});
});