Skip to content

Commit

Permalink
Extend ReDoS testing with general performance tests (#1267)
Browse files Browse the repository at this point in the history
Add general performance/ReDoS tests modeled after `fast-check`'s blog
post on the topic[1].

This complements the ReDoS regression tests (for Unix) in that these
tests will only catch some ReDoS regression. In fact, based on testing,
this project's ReDoS CVEs wouldn't have been caught by the tests being
added here.

--
1. https://fast-check.dev/blog/2023/10/05/finding-back-a-redos-vulnerability-in-zod/
  • Loading branch information
ericcornelissen authored Oct 28, 2023
1 parent 1008fe9 commit 2a91da5
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 7 deletions.
33 changes: 33 additions & 0 deletions test/unit/_macros.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
* @license MIT
*/

import { performance } from "node:perf_hooks";

import test from "ava";
import fc from "fast-check";

/**
* Transforms a string by replacing control characters with unicode point codes
Expand Down Expand Up @@ -94,6 +97,36 @@ export const flag = test.macro({
},
});

/**
* The flag macro tests the behaviour of the function returned by the provided
* `getFlagProtectionFunction`.
*
* @param {object} t The AVA test object.
* @param {object} args The arguments for this function.
* @param {any} args.arbitraries The arbitraries to test with.
* @param {number} args.maxMillis The maximum duration in milliseconds.
* @param {Function} args.setup A function to setup the function to test.
*/
export const duration = test.macro({
exec(t, { arbitraries, maxMillis, setup }) {
fc.assert(
fc.property(...arbitraries, (...args) => {
const fn = setup();

const startTime = performance.now();
try {
fn(...args);
} catch (_) {
// not concerned about functional correctness
}
const endTime = performance.now();

t.true(endTime - startTime < maxMillis);
}),
);
},
});

/**
* The quote macro tests the behaviour of the function returned by the provided
* `getQuoteFunction`.
Expand Down
35 changes: 28 additions & 7 deletions test/unit/unix/shells.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ for (const [shellName, shellExports] of Object.entries(shells)) {
t.is(typeof result, "string");
});

test(`escape performance for ${shellName}`, macros.duration, {
arbitraries: [fc.string({ size: "xlarge" })],
maxMillis: 50,
setup: shellExports.getEscapeFunction,
});

redosFixtures.forEach((input, id) => {
test(`${shellName}, ReDoS #${id}`, (t) => {
const escape = shellExports.getEscapeFunction();
escape(input);
t.pass();
});
});

flagFixtures.forEach(({ input, expected }) => {
test(macros.flag, {
expected: expected.unquoted,
Expand All @@ -63,6 +77,12 @@ for (const [shellName, shellExports] of Object.entries(shells)) {
},
);

test(`flag protection performance for ${shellName}`, macros.duration, {
arbitraries: [fc.string({ size: "xlarge" })],
maxMillis: 50,
setup: shellExports.getFlagProtectionFunction,
});

if (shellExports !== nosh) {
quoteFixtures.forEach(({ input, expected }) => {
test(macros.quote, {
Expand All @@ -80,13 +100,14 @@ for (const [shellName, shellExports] of Object.entries(shells)) {
const result = quoteFn(intermediate);
t.is(typeof result, "string");
});
}

redosFixtures.forEach((input, id) => {
test(`${shellName}, ReDoS #${id}`, (t) => {
const escape = shellExports.getEscapeFunction();
escape(input);
t.pass();
test(`quote performance for ${shellName}`, macros.duration, {
arbitraries: [fc.string({ size: "xlarge" })],
maxMillis: 50,
setup: () => {
const [escapeFn, quoteFn] = shellExports.getQuoteFunction();
return (arg) => quoteFn(escapeFn(arg));
},
});
});
}
}
21 changes: 21 additions & 0 deletions test/unit/win/shells.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ for (const [shellName, shellExports] of Object.entries(shells)) {
t.is(typeof result, "string");
});

test(`escape performance for ${shellName}`, macros.duration, {
arbitraries: [fc.string({ size: "xlarge" })],
maxMillis: 50,
setup: shellExports.getEscapeFunction,
});

flagFixtures.forEach(({ input, expected }) => {
test(macros.flag, {
expected: expected.unquoted,
Expand All @@ -58,6 +64,12 @@ for (const [shellName, shellExports] of Object.entries(shells)) {
},
);

test(`flag protection performance for ${shellName}`, macros.duration, {
arbitraries: [fc.string({ size: "xlarge" })],
maxMillis: 50,
setup: shellExports.getFlagProtectionFunction,
});

if (shellExports !== nosh) {
quoteFixtures.forEach(({ input, expected }) => {
test(macros.quote, {
Expand All @@ -75,5 +87,14 @@ for (const [shellName, shellExports] of Object.entries(shells)) {
const result = quoteFn(intermediate);
t.is(typeof result, "string");
});

test(`quote performance for ${shellName}`, macros.duration, {
arbitraries: [fc.string({ size: "xlarge" })],
maxMillis: 50,
setup: () => {
const [escapeFn, quoteFn] = shellExports.getQuoteFunction();
return (arg) => quoteFn(escapeFn(arg));
},
});
}
}

0 comments on commit 2a91da5

Please sign in to comment.