tjenkinson / redos-detector

A CLI and library which tests with certainty if a regex pattern is safe from ReDoS attacks. Supported in the browser, Node and Deno.
https://redosdetector.com
MIT License
43 stars 4 forks source link
parser pattern redos redos-checker redos-detector regex regexp

redos-detector

A CLI and library which tests with certainty if a regex pattern is safe from ReDoS attacks. Supported in the browser, Node and Deno.

By default it considers a pattern unsafe if there are potentially more than 200 backtracks possible.

There are some cases where it may report a pattern as unsafe when in reality it's safe, but it should never report a pattern as safe when it is not.

Note that this tool checks patterns that are expected to run using an engine that implements the ECMA-262 (JS) standard. If your pattern will be run in a different engine please be aware the result given here could be incorrect due to differences in engine behaviour.

Demo

https://redosdetector.com/ [Source]

Examples

Good

isSafe(/^([a-c]*)d([a-c]*)$/).safe === true;

Demo

because for any given input string this can only match in one way, or not match.

Bad

isSafe(/^([a-b]*)([a-c]*)$/).safe === false;

Demo

because the input a could match in both places. The input ax could result in both being tried.

The CLI would output the following for this:

Regex is not safe. There could be infinite backtracks.

The following trails show how the same input can be matched multiple ways.
2: `[a-b]` | 10: `[a-c]`
========================
 2: `[a-b]` | 10: `[a-c]`
10: `[a-c]` | 10: `[a-c]`
=========================
 2: `[a-b]` | 10: `[a-c]`
10: `[a-c]` | 10: `[a-c]`
10: `[a-c]` | 10: `[a-c]`
=========================
2: `[a-b]` | 10: `[a-c]`
2: `[a-b]` | 10: `[a-c]`
========================

Hit maximum number of backtracks so there may be more results than shown here.
Note there may be more results than shown here as some infinite loops are detected and removed.

The first result means you could have a match where given the same input string, [a-c] takes a character (at position 10), or [a-b] takes a character (at position 2).

Note this could be made good again by making the first group atomic. Atomic groups are not supported directly right now, but can be inferred using a pattern like ^(?=([a-b]*))\1([a-c]*)$ (Demo).

isSafe(/^(a|a)+$/).safe === false;

Demo

is bad because in the group a could match on both sides. The input aaaaaax could result in many combinations being tried.

How does it work?

This tool will generate all the combinations of input strings that would match the pattern, and will output any input string that could match the pattern in multiple ways.

Usage

This can be used via the CLI, or as a library. It's on npm.

There's also an ESLint plugin "eslint-plugin-redos-detector".

Result Structure

The following is the structure of the result you will get from both isSafe, isSafePattern and the CLI with the --json flag.

Root

type Root = {
  safe: boolean;
  error: null | 'hitMaxBacktracks' | 'hitMaxSteps' | 'timedOut';
  trails: Trail[];
  patternDowngraded: boolean;
  pattern: string;
  worstCaseBacktrackCount:
    | {
        infinite: true;
      }
    | {
        infinite: false;
        value: number;
      };
};

Trail

type Trail = {
  trail: {
    a: Side;
    b: Side;
  }[];
};

Side

type Side = {
  backreferenceStack: {
    index: number;
    node: Node;
  }[];
  node: Node;
  quantifierIterations: {
    iteration: number;
    node: Node;
  }[];
};

Node

type Node = {
  start: Location;
  end: Location;
  source: string;
};

Location

type Location = {
  offset: number;
};

Options

The following options exist for both the library and CLI:

Note it's possible for there to be a infinite number of results, so you should probably make sure at least one of the maxSteps and timeout options is set to a finite number.

CLI

$ npx redos-detector check "<regex pattern>" (--caseInsensitive) (--unicode) (--dotAll) (--multiLine) (--maxBacktracks <number>) (--maxSteps <number>) (--timeout <number>) (--alwaysIncludeTrails) (--disableDowngrade) (--resultsLimit <number>) (--json)

to run on the fly or

$ npm i -g redos-detector

to make the command available globally as redos-detector.

By default this will output the result in a text format, and the exit status will be 0 if the pattern is safe, otherwise non-0. You should not try and parse this text output as it may change between any version.

Only the first 15 results will be shown in the text output, but this can be changed with the --resultsLimit option. This option is ignored with --json.

The --json option will result in JSON being outputted containing more information. The structure of this will follow semantic versioning. When this option is used the exit status will always be 0 (unless an exception occurred), so you should always check the safe property to determine if the pattern is safe.

Library

$ npm install redos-detector

The following functions are provided:

Useful Resources

Here are some great resources, which I found super helpful when building this.