Skip to content

Commit e6f66d9

Browse files
authored
feat(eslint-plugin-devtools): add rule forbidding non-null prototypes for Records (#604)
`Record` types in TS are typically used to indicate that an object may have arbitrary keys. This means that using `{}` or similar object literals to initialize them is a poor choice -- if user code can cause keys like `hasOwnProperty` or `__proto__` to be used as keys on those objects, they will not behave as intended. This has been the cause of many security issues in the JS ecosystem, so let's add a lint rule to limit impact of these issues and force ourselves to stick to best practices.
1 parent 2f60007 commit e6f66d9

File tree

14 files changed

+973
-666
lines changed

14 files changed

+973
-666
lines changed

configs/eslint-config-devtools/common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const tsRules = {
2121
'error',
2222
{ prefer: 'type-imports' },
2323
],
24+
'@mongodb-js/devtools/no-plain-object-records': 'error',
2425
};
2526

2627
const tsxRules = {

configs/eslint-plugin-devtools/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
module.exports = {
33
rules: {
44
'no-expect-method-without-call': require('./rules/no-expect-method-without-call'),
5+
'no-plain-object-records': require('./rules/no-plain-object-records'),
56
},
67
};

configs/eslint-plugin-devtools/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
"devDependencies": {
2929
"@mongodb-js/mocha-config-devtools": "^1.0.5",
3030
"@mongodb-js/prettier-config-devtools": "^1.0.1",
31+
"@typescript-eslint/rule-tester": "^8.49.0",
3132
"depcheck": "^1.4.7",
32-
"eslint": "^7.25.0",
33+
"eslint": "^7.25.0 || ^8.0.0",
3334
"mocha": "^8.4.0",
3435
"nyc": "^15.1.0"
3536
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
/** @type {import('eslint').Rule.RuleModule} */
4+
module.exports = {
5+
meta: {
6+
type: 'problem',
7+
docs: {
8+
description:
9+
'Using {} to initialize records means that keys present on Object.prototype will not be handled properly',
10+
},
11+
fixable: 'code',
12+
schema: [],
13+
},
14+
create(context) {
15+
return {
16+
VariableDeclarator(node) {
17+
if (
18+
node.id.type === 'Identifier' &&
19+
node.id.typeAnnotation &&
20+
node.id.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
21+
node.id.typeAnnotation.typeAnnotation.typeName.name === 'Record' &&
22+
node.init &&
23+
node.init.type === 'ObjectExpression' &&
24+
!node.init.properties.some(
25+
(prop) =>
26+
prop.key.type === 'Identifier' && prop.key.name === '__proto__',
27+
)
28+
) {
29+
context.report({
30+
node,
31+
message:
32+
'{} is not a good initializer for records. Use Object.create(null) instead.',
33+
fix:
34+
node.init.properties.length === 0
35+
? (fixer) => fixer.replaceText(node.init, 'Object.create(null)')
36+
: (fixer) =>
37+
fixer.insertTextBefore(
38+
node.init.properties[0],
39+
'__proto__: null, ',
40+
),
41+
});
42+
}
43+
},
44+
};
45+
},
46+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
const { RuleTester } = require('@typescript-eslint/rule-tester');
3+
const rule = require('./no-plain-object-records');
4+
5+
RuleTester.afterAll = after;
6+
const ruleTester = new RuleTester();
7+
8+
ruleTester.run('no-plain-object-records', rule, {
9+
valid: [
10+
{
11+
code: 'const record: Record<string, number> = Object.create(null);',
12+
},
13+
{
14+
code: 'const record: Record<string, number> = {__proto__: null, a: 1, b: 2};',
15+
},
16+
{
17+
code: 'const record: Record<string, number> = {__proto__: 42, a: 1, b: 2};',
18+
},
19+
{
20+
code: 'const record: Record<string, number> = {a, b, __proto__: 42};',
21+
},
22+
],
23+
24+
invalid: [
25+
{
26+
code: 'const record: Record<string, number> = {a, b};',
27+
output: 'const record: Record<string, number> = {__proto__: null, a, b};',
28+
errors: [
29+
{
30+
message:
31+
'{} is not a good initializer for records. Use Object.create(null) instead.',
32+
},
33+
],
34+
},
35+
{
36+
code: 'const record: Record<string, unknown> = {};',
37+
output: 'const record: Record<string, unknown> = Object.create(null);',
38+
errors: [
39+
{
40+
message:
41+
'{} is not a good initializer for records. Use Object.create(null) instead.',
42+
},
43+
],
44+
},
45+
{
46+
code: 'let foo, record: Record<string, unknown> = {};',
47+
output: 'let foo, record: Record<string, unknown> = Object.create(null);',
48+
errors: [
49+
{
50+
message:
51+
'{} is not a good initializer for records. Use Object.create(null) instead.',
52+
},
53+
],
54+
},
55+
],
56+
});

0 commit comments

Comments
 (0)