Skip to content

Commit b1beb61

Browse files
bdoughertysindresorhus
authored andcommitted
Add no-unsafe-regex rule (#146)
1 parent 1c5fdf0 commit b1beb61

File tree

6 files changed

+148
-3
lines changed

6 files changed

+148
-3
lines changed

docs/rules/no-unsafe-regex.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Disallow unsafe regular expressions
2+
3+
Uses [safe-regex](https://github.com/substack/safe-regex) to disallow potentially [catastrophic](http://regular-expressions.mobi/catastrophic.html) [exponential-time](http://perlgeek.de/blog-en/perl-tips/in-search-of-an-exponetial-regexp.html) regular expressions.
4+
5+
## Fail
6+
7+
```js
8+
const regex = /^(a?){25}(a){25}$/;
9+
const regex = RegExp(Array(27).join('a?') + Array(27).join('a'));
10+
const regex = /(x+x+)+y/;
11+
const regex = /foo|(x+x+)+y/;
12+
const regex = /(a+){10}y/;
13+
const regex = /(a+){2}y/;
14+
const regex = /(.*){1,32000}[bc]/;
15+
```
16+
17+
18+
## Pass
19+
20+
```js
21+
const regex = /\bOakland\b/;
22+
const regex = /\b(Oakland|San Francisco)\b/i;
23+
const regex = /^\d+1337\d+$/i;
24+
const regex = /^\d+(1337|404)\d+$/i;
25+
const regex = /^\d+(1337|404)*\d+$/i;
26+
const regex = RegExp(Array(26).join('a?') + Array(26).join('a'));
27+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ module.exports = {
3333
'unicorn/new-for-builtins': 'error',
3434
'unicorn/regex-shorthand': 'error',
3535
'unicorn/prefer-spread': 'error',
36-
'unicorn/error-message': 'error'
36+
'unicorn/error-message': 'error',
37+
'unicorn/no-unsafe-regex': 'error'
3738
}
3839
}
3940
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"lodash.camelcase": "^4.1.1",
3939
"lodash.kebabcase": "^4.0.1",
4040
"lodash.snakecase": "^4.0.1",
41-
"lodash.upperfirst": "^4.2.0"
41+
"lodash.upperfirst": "^4.2.0",
42+
"safe-regex": "^1.1.0"
4243
},
4344
"devDependencies": {
4445
"ava": "*",

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ Configure it in `package.json`.
5252
"unicorn/new-for-builtins": "error",
5353
"unicorn/regex-shorthand": "error",
5454
"unicorn/prefer-spread": "error",
55-
"unicorn/error-message": "error"
55+
"unicorn/error-message": "error",
56+
"unicorn/no-unsafe-regex": "error"
5657
}
5758
}
5859
}
@@ -81,6 +82,7 @@ Configure it in `package.json`.
8182
- [regex-shorthand](docs/rules/regex-shorthand.md) - Enforce the use of regex shorthands to improve readability. *(fixable)*
8283
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
8384
- [error-message](docs/rules/error-message.md) - Enforce passing a `message` value when throwing a built-in error.
85+
- [no-unsafe-regex](docs/rules/no-unsafe-regex.md) - Disallow unsafe regular expressions.
8486

8587

8688
## Recommended config

rules/no-unsafe-regex.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
const safeRegex = require('safe-regex');
3+
const getDocsUrl = require('./utils/get-docs-url');
4+
5+
const message = 'Unsafe regular expression.';
6+
7+
const create = context => {
8+
return {
9+
'Literal[regex]': node => {
10+
// Handle regex literal inside RegExp constructor in the other handler
11+
if (node.parent.type === 'NewExpression' && node.parent.callee.name === 'RegExp') {
12+
return;
13+
}
14+
15+
if (!safeRegex(node.value)) {
16+
context.report({
17+
node,
18+
message
19+
});
20+
}
21+
},
22+
'NewExpression[callee.name="RegExp"]': node => {
23+
const args = node.arguments;
24+
25+
if (args.length === 0 || args[0].type !== 'Literal') {
26+
return;
27+
}
28+
29+
const hasRegExp = args[0].regex;
30+
31+
let pattern = null;
32+
let flags = null;
33+
34+
if (hasRegExp) {
35+
pattern = args[0].regex.pattern;
36+
flags = args[1] && args[1].type === 'Literal' ? args[1].value : args[0].regex.flags;
37+
} else {
38+
pattern = args[0].value;
39+
flags = args[1] && args[1].type === 'Literal' ? args[1].value : '';
40+
}
41+
42+
if (!safeRegex(`/${pattern}/${flags}`)) {
43+
context.report({
44+
node,
45+
message
46+
});
47+
}
48+
}
49+
};
50+
};
51+
52+
module.exports = {
53+
create,
54+
meta: {
55+
docs: {
56+
url: getDocsUrl()
57+
}
58+
}
59+
};

test/no-unsafe-regex.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../rules/no-unsafe-regex';
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
},
9+
parserOptions: {
10+
sourceType: 'module'
11+
}
12+
});
13+
14+
const error = {
15+
ruleId: 'no-unsafe-regex',
16+
message: 'Unsafe regular expression.'
17+
};
18+
19+
ruleTester.run('no-unsafe-regex', rule, {
20+
valid: [
21+
'const foo = /\bunicorn\b/',
22+
'const foo = /\bunicorn\b/g',
23+
`const foo = new RegExp('^\bunicorn\b')`,
24+
`const foo = new RegExp('^\bunicorn\b', 'i')`,
25+
'const foo = new RegExp(/\bunicorn\b/)',
26+
'const foo = new RegExp(/\bunicorn\b/g)',
27+
'const foo = new RegExp()'
28+
],
29+
invalid: [
30+
{
31+
code: 'const foo = /(x+x+)+y/',
32+
errors: [error]
33+
},
34+
{
35+
code: 'const foo = /(x+x+)+y/g',
36+
errors: [error]
37+
},
38+
{
39+
code: `const foo = new RegExp('(x+x+)+y')`,
40+
errors: [error]
41+
},
42+
{
43+
code: `const foo = new RegExp('(x+x+)+y', 'g')`,
44+
errors: [error]
45+
},
46+
{
47+
code: `const foo = new RegExp(/(x+x+)+y/)`,
48+
errors: [error]
49+
},
50+
{
51+
code: `const foo = new RegExp(/(x+x+)+y/g)`,
52+
errors: [error]
53+
}
54+
]
55+
});

0 commit comments

Comments
 (0)