UNPKG

eslint-plugin-redos

Version:

ESLint plugin for catching ReDoS vulnerability

402 lines (394 loc) 12 kB
"use strict"; 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 } };