eslint-plugin-redos
Version:
ESLint plugin for catching ReDoS vulnerability
402 lines (394 loc) • 12 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/utils/cache.ts
var fs, os, path, __mock__, findDefaultCacheFile, findCacheFileFromOptions, findCacheFile;
var init_cache = __esm({
"src/utils/cache.ts"() {
"use strict";
fs = __toESM(require("fs"));
os = __toESM(require("os"));
path = __toESM(require("path"));
__mock__ = {
nodeModuleDir: path.join(
require.resolve("eslint-plugin-redos/package.json"),
"../.."
)
};
findDefaultCacheFile = () => {
let cacheDir = null;
try {
cacheDir = path.join(__mock__.nodeModuleDir, ".cache/eslint-plugin-redos");
fs.mkdirSync(cacheDir, { recursive: true });
} catch {
cacheDir = os.tmpdir();
}
const cacheFile = path.join(cacheDir, "recheck-cache.json");
return cacheFile;
};
findCacheFileFromOptions = (location) => {
const cacheFile = path.resolve(location);
let stat;
try {
stat = fs.statSync(cacheFile);
} catch {
}
if (stat) {
if (stat.isDirectory()) {
throw new Error(`Resolved cache.location '${cacheFile}' is a directory`);
}
}
return cacheFile;
};
findCacheFile = (location) => {
if (!location) {
return findDefaultCacheFile();
}
return findCacheFileFromOptions(location);
};
}
});
// src/utils/version.ts
var recheckVersion;
var init_version = __esm({
"src/utils/version.ts"() {
"use strict";
recheckVersion = () => {
const pkg = require("recheck/package.json");
return pkg.version;
};
}
});
// src/utils/checker.ts
var fs2, util, ReDoS, createCachedCheck;
var init_checker = __esm({
"src/utils/checker.ts"() {
"use strict";
fs2 = __toESM(require("fs"));
util = __toESM(require("util"));
ReDoS = __toESM(require("recheck"));
init_cache();
init_version();
createCachedCheck = (cache, timeout, params) => {
const {
location: cacheLocation = void 0,
strategy: cacheStrategy = "conservative"
} = typeof cache === "boolean" ? {} : cache;
const cacheFile = cache ? findCacheFile(cacheLocation) : null;
const settings = {
version: recheckVersion(),
timeout,
strategy: cacheStrategy,
params
};
let cacheData;
try {
if (cacheFile) {
cacheData = fs2.existsSync(cacheFile) ? JSON.parse(fs2.readFileSync(cacheFile, "utf-8")) : {};
if (!util.isDeepStrictEqual(cacheData.settings, settings)) {
try {
fs2.rmSync(cacheFile);
} catch {
}
cacheData = {
settings,
results: {}
};
}
}
} catch (error) {
throw new Error(`Invalid cache: ${error}`);
}
const cachedCheck = (source, flags) => {
const key = `/${source}/${flags}`;
if (cacheData && cacheData.results[key]) {
return cacheData.results[key];
}
const result = ReDoS.checkSync(source, flags, { timeout, ...params });
let shouldCache = false;
switch (cacheStrategy) {
case "aggressive":
shouldCache = true;
break;
case "conservative":
shouldCache = result.checker === "automaton";
break;
}
if (!shouldCache) {
return result;
}
if (cacheFile) {
cacheData.results[key] = result;
fs2.writeFileSync(cacheFile, JSON.stringify(cacheData));
}
return result;
};
return cachedCheck;
};
}
});
// src/rules/no-vulnerable.ts
var require_no_vulnerable = __commonJS({
"src/rules/no-vulnerable.ts"(exports2, module2) {
"use strict";
init_checker();
var rule = {
meta: {
type: "problem",
docs: {
description: "disallow ReDoS vulnerable RegExp literals"
},
schema: [
{
properties: {
ignoreErrors: {
type: "boolean"
},
permittableComplexities: {
type: "array",
items: {
enum: ["polynomial", "exponential"]
},
additionalItems: false,
uniqueItems: true
},
cache: {
type: ["boolean", "object"],
properties: {
location: {
type: "string"
},
strategy: {
type: "string",
enum: ["aggressive", "conservative"]
}
},
additionalProperties: false
},
accelerationMode: {
type: "string",
enum: ["auto", "on", "off"]
},
attackLimit: {
type: "number"
},
attackTimeout: {
type: ["number", "null"]
},
checker: {
type: "string",
enum: ["auto", "automaton", "fuzz"]
},
crossoverSize: {
type: "number"
},
heatRatio: {
type: "number"
},
incubationLimit: {
type: "number"
},
incubationTimeout: {
type: ["number", "null"]
},
maxAttackStringSize: {
type: "number"
},
maxDegree: {
type: "number"
},
maxGeneStringSize: {
type: "number"
},
maxGenerationSize: {
type: "number"
},
maxInitialGenerationSize: {
type: "number"
},
maxIteration: {
type: "number"
},
maxNFASize: {
type: "number"
},
maxPatternSize: {
type: "number"
},
maxRecallStringSize: {
type: "number"
},
maxRepeatCount: {
type: "number"
},
maxSimpleRepeatCount: {
type: "number"
},
mutationSize: {
type: "number"
},
randomSeed: {
type: "number"
},
recallLimit: {
type: "number"
},
recallTimeout: {
type: ["number", "null"]
},
seeder: {
type: "string",
enum: ["static", "dynamic"]
},
seedingLimit: {
type: "number"
},
seedingTimeout: {
type: ["number", "null"]
},
timeout: {
type: ["number", "null"]
}
},
additionalProperties: false
}
]
},
create: (context) => {
const options = context.options[0] || {};
const {
ignoreErrors = true,
permittableComplexities = [],
timeout = 1e4,
cache = false,
...params
} = options;
const cachedCheck = createCachedCheck(cache, timeout, params);
const check = (node, source, flags) => {
const result = cachedCheck(source, flags);
switch (result.status) {
case "safe":
break;
case "vulnerable":
if (permittableComplexities.includes(result.complexity.type)) {
break;
}
switch (result.complexity.type) {
case "exponential":
case "polynomial":
context.report({
message: `Found a ReDoS vulnerable RegExp (${result.complexity.summary}).`,
node
});
break;
}
break;
case "unknown":
if (ignoreErrors) {
break;
}
switch (result.error.kind) {
case "timeout":
context.report({
message: `Error on ReDoS vulnerability check: timeout`,
node
});
break;
case "invalid":
case "unsupported":
context.report({
message: `Error on ReDoS vulnerability check: ${result.error.message} (${result.error.kind})`,
node
});
break;
}
}
};
const isCallOrNewRegExp = (node) => {
if (!(node.callee.type === "Identifier" && node.callee.name === "RegExp")) {
return false;
}
if (!(node.arguments.length == 1 || node.arguments.length == 2)) {
return false;
}
if (!node.arguments.every(
(arg) => arg.type === "Literal" && typeof arg.value === "string"
)) {
return false;
}
return true;
};
return {
Literal: (node) => {
if (!(node.value instanceof RegExp)) {
return;
}
const { source, flags } = node.value;
check(node, source, flags);
},
NewExpression: (node) => {
if (!isCallOrNewRegExp(node)) {
return;
}
const [source, flags = ""] = node.arguments.map(
(arg) => arg.value
);
check(node, source, flags);
},
CallExpression: (node) => {
if (!isCallOrNewRegExp(node)) {
return;
}
const [source, flags = ""] = node.arguments.map(
(arg) => arg.value
);
check(node, source, flags);
}
};
}
};
module2.exports = rule;
}
});
// src/main.ts
var import_no_vulnerable = __toESM(require_no_vulnerable());
// src/configs/recommended.ts
var recommended_default = {
plugins: ["redos"],
rules: {
"redos/no-vulnerable": "error"
}
};
// src/main.ts
module.exports = {
rules: {
"no-vulnerable": import_no_vulnerable.default
},
configs: { recommended: recommended_default }
};