Fix pretty recipe parser ReDoS (#2687)
Co-authored-by: zainnadeem(RedOpsCell) <zainnadeemzainnadeem80@gmail.com> (main author)
This commit is contained in:
parent
1343a107cb
commit
f77ddf4890
@ -1015,6 +1015,7 @@ class Utils {
|
||||
|
||||
// Parse bespoke recipe format
|
||||
recipe = recipe.replace(/\n/g, "");
|
||||
Utils._validatePrettyRecipe(recipe);
|
||||
let m, args;
|
||||
const recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/'])*)(\/[^)]+)?\)/g,
|
||||
recipeConfig = [];
|
||||
@ -1040,6 +1041,53 @@ class Utils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs a linear structural validation pass over pretty recipe syntax.
|
||||
*
|
||||
* @param {string} recipe
|
||||
* @throws {Error} if the recipe is structurally invalid
|
||||
*/
|
||||
static _validatePrettyRecipe(recipe) {
|
||||
let i = 0;
|
||||
|
||||
while (i < recipe.length) {
|
||||
const openParen = recipe.indexOf("(", i);
|
||||
if (openParen === -1 || openParen === i) {
|
||||
throw new Error("Invalid recipe");
|
||||
}
|
||||
|
||||
i = openParen + 1;
|
||||
let inString = false,
|
||||
escaped = false,
|
||||
foundCloseParen = false;
|
||||
|
||||
for (; i < recipe.length; i++) {
|
||||
const c = recipe[i];
|
||||
|
||||
if (inString) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (c === "\\") {
|
||||
escaped = true;
|
||||
} else if (c === "'") {
|
||||
inString = false;
|
||||
}
|
||||
} else if (c === "'") {
|
||||
inString = true;
|
||||
} else if (c === ")") {
|
||||
foundCloseParen = true;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundCloseParen || inString || escaped) {
|
||||
throw new Error("Invalid recipe");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats a list of files or directories.
|
||||
*
|
||||
|
||||
@ -26,4 +26,81 @@ TestRegister.addApiTests([
|
||||
"\x7e...",
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should parse normal pretty recipes", () => {
|
||||
assert.deepStrictEqual(
|
||||
Utils.parseRecipeConfig("From_Base64('A-Za-z0-9+/=',true)To_Hex('Space')"),
|
||||
[
|
||||
{
|
||||
op: "From Base64",
|
||||
args: ["A-Za-z0-9+/=", true],
|
||||
},
|
||||
{
|
||||
op: "To Hex",
|
||||
args: ["Space"],
|
||||
},
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should parse pretty recipe options", () => {
|
||||
assert.deepStrictEqual(
|
||||
Utils.parseRecipeConfig("A(/disabled/breakpoint)"),
|
||||
[
|
||||
{
|
||||
op: "A",
|
||||
args: [],
|
||||
disabled: true,
|
||||
breakpoint: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should parse escaped quotes and backslashes in pretty recipes", () => {
|
||||
assert.deepStrictEqual(
|
||||
Utils.parseRecipeConfig("A('\\'\\\\')"),
|
||||
[
|
||||
{
|
||||
op: "A",
|
||||
args: ["'\\"],
|
||||
},
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should parse large valid quoted pretty recipe arguments", () => {
|
||||
const value = "x".repeat(10000);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
Utils.parseRecipeConfig(`A('${value}')`),
|
||||
[
|
||||
{
|
||||
op: "A",
|
||||
args: [value],
|
||||
},
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should reject malformed pretty recipes with unmatched quotes", () => {
|
||||
assert.throws(
|
||||
() => Utils.parseRecipeConfig("A(" + "'".repeat(10000)),
|
||||
/Invalid recipe/,
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should reject malformed pretty recipes with malformed parentheses", () => {
|
||||
assert.throws(
|
||||
() => Utils.parseRecipeConfig("A("),
|
||||
/Invalid recipe/,
|
||||
);
|
||||
}),
|
||||
|
||||
it("Utils: should reject malformed pretty recipes with malformed escapes", () => {
|
||||
assert.throws(
|
||||
() => Utils.parseRecipeConfig("A('" + "\\".repeat(10000)),
|
||||
/Invalid recipe/,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user