Skip to content
Closed
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
36 changes: 36 additions & 0 deletions src/__tests__/security.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// PoC scripts adapted from: https://gist.github.com/bx33661/581e3a38134601c04e19b4dfc9b459b9
// Credit: https://github.com/bx33661

import test from 'ava';
import postcss from 'postcss';

import parser from '../index.js';

function buildDeepPseudoTree (depth) {
const { pseudo, selector, tag } = parser;
let current = selector({ nodes: [tag({value: 'a'})] });
for (let i = 0; i < depth; i++) {
const p = pseudo({ value: ':not', nodes: [current] });
current = selector({ nodes: [p] });
}
return current;
}

test('toString() does not stack overflow on deeply nested programmatic tree', t => {
const deep = buildDeepPseudoTree(1000);
t.notThrows(() => deep.toString());
});

test('processSync() with deep selector does not stack overflow', t => {
const depth = 1000;
const malicious = ':where('.repeat(depth) + 'a' + ')'.repeat(depth);
const rule = postcss.parse(malicious + ' {}').first;
t.notThrows(() =>
parser((selectors) => {}).processSync(rule, { updateSelector: true })
);
});

test('clone() does not stack overflow on deeply nested programmatic tree', t => {
const deep = buildDeepPseudoTree(1100);
t.notThrows(() => deep.clone());
});
57 changes: 56 additions & 1 deletion src/selectors/container.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
import Node from './node';
import * as types from './types';

export function toStringIterative (root) {
const parts = [];
const stack = [root];

while (stack.length > 0) {
const item = stack.pop();

if (typeof item === 'string') {
parts.push(item);
continue;
}

const node = item;

if (!node.nodes) {
parts.push(node.toString());
continue;
}

const children = node.nodes;

if (node.type === types.ROOT) {
if (node.trailingComma) {
stack.push(',');
}
for (let i = children.length - 1; i >= 0; i--) {
if (i < children.length - 1) {
stack.push(',');
}
stack.push(children[i]);
}
} else if (node.type === types.PSEUDO) {
stack.push(node.rawSpaceAfter);
if (children.length > 0) {
stack.push(')');
for (let i = children.length - 1; i >= 0; i--) {
if (i < children.length - 1) {
stack.push(',');
}
stack.push(children[i]);
}
stack.push('(');
}
stack.push(node.stringifyProperty('value'));
stack.push(node.rawSpaceBefore);
} else {
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]);
}
}
}

return parts.join('');
}

export default class Container extends Node {
constructor (opts) {
super(opts);
Expand Down Expand Up @@ -325,6 +380,6 @@ export default class Container extends Node {
}

toString () {
return this.map(String).join('');
return toStringIterative(this);
}
}
41 changes: 28 additions & 13 deletions src/selectors/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,37 @@ let cloneNode = function (obj, parent) {
}

let cloned = new obj.constructor();
const workQueue = [{source: obj, cloned, parent}];
let head = 0;

for ( let i in obj ) {
if ( !obj.hasOwnProperty(i) ) {
continue;
}
let value = obj[i];
let type = typeof value;
while (head < workQueue.length) {
const {source: src, cloned: dst, parent: p} = workQueue[head++];

if ( i === 'parent' && type === 'object' ) {
if (parent) {
cloned[i] = parent;
for (let i in src) {
if (!src.hasOwnProperty(i)) {
continue;
}
let value = src[i];
let type = typeof value;

if (i === 'parent' && type === 'object') {
if (p) {
dst[i] = p;
}
} else if (i === 'nodes' && value instanceof Array) {
dst[i] = value.map(j => {
if (typeof j !== 'object' || j === null) {
return j;
}
const childCloned = new j.constructor();
workQueue.push({source: j, cloned: childCloned, parent: dst});
return childCloned;
});
} else if (value instanceof Array) {
dst[i] = value.map(j => cloneNode(j, dst));
} else {
dst[i] = cloneNode(value, dst);
}
} else if ( value instanceof Array ) {
cloned[i] = value.map( j => cloneNode(j, cloned) );
} else {
cloned[i] = cloneNode(value, cloned);
}
}

Expand Down
10 changes: 2 additions & 8 deletions src/selectors/pseudo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Container from './container';
import Container, {toStringIterative} from './container';
import {PSEUDO} from './types';

export default class Pseudo extends Container {
Expand All @@ -8,12 +8,6 @@ export default class Pseudo extends Container {
}

toString () {
let params = this.length ? '(' + this.map(String).join(',') + ')' : '';
return [
this.rawSpaceBefore,
this.stringifyProperty("value"),
params,
this.rawSpaceAfter,
].join('');
return toStringIterative(this);
}
}
8 changes: 2 additions & 6 deletions src/selectors/root.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Container from './container';
import Container, {toStringIterative} from './container';
import {ROOT} from './types';

export default class Root extends Container {
Expand All @@ -8,11 +8,7 @@ export default class Root extends Container {
}

toString () {
let str = this.reduce((memo, selector) => {
memo.push(String(selector));
return memo;
}, []).join(',');
return this.trailingComma ? str + ',' : str;
return toStringIterative(this);
}

error (message, options) {
Expand Down