Skip to content

Commit 3650733

Browse files
Require __proto__: null for non-empty dictionary literals, Object.create(null) for empty ones
Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/39bd15e2-0089-46b6-99ac-4bd914e5ba0a Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com>
1 parent 602a68b commit 3650733

2 files changed

Lines changed: 52 additions & 16 deletions

File tree

eslint/eslint-plugin/src/null-prototype-dictionaries.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
44
import type { TSESTree, TSESLint, ParserServices } from '@typescript-eslint/utils';
55
import type * as ts from 'typescript';
66

7-
type MessageIds = 'error-object-literal-dictionary';
7+
type MessageIds = 'error-empty-object-literal-dictionary' | 'error-missing-null-prototype';
88
type Options = [];
99

1010
const nullPrototypeDictionariesRule: TSESLint.RuleModule<MessageIds, Options> = {
1111
defaultOptions: [],
1212
meta: {
1313
type: 'problem',
1414
messages: {
15-
'error-object-literal-dictionary':
15+
'error-empty-object-literal-dictionary':
1616
'Dictionary objects typed as Record<string, T> should be created using Object.create(null)' +
17-
' instead of an object literal. This avoids prototype pollution, collisions with' +
17+
' instead of an empty object literal. This avoids prototype pollution, collisions with' +
1818
' Object.prototype members such as "toString", and enables higher performance since runtimes' +
1919
' such as V8 process Object.create(null) as opting out of having a hidden class and going' +
20-
' directly to dictionary mode.'
20+
' directly to dictionary mode.',
21+
'error-missing-null-prototype':
22+
'Dictionary object literals typed as Record<string, T> should include "__proto__: null"' +
23+
' to avoid prototype pollution and collisions with Object.prototype members such as "toString".'
2124
},
2225
schema: [],
2326
docs: {
@@ -76,10 +79,34 @@ const nullPrototypeDictionariesRule: TSESLint.RuleModule<MessageIds, Options> =
7679
return;
7780
}
7881

79-
if (isStringKeyedDictionaryType(contextualType)) {
82+
if (!isStringKeyedDictionaryType(contextualType)) {
83+
return;
84+
}
85+
86+
// For empty object literals, recommend Object.create(null) which is more performant
87+
if (node.properties.length === 0) {
88+
context.report({
89+
node,
90+
messageId: 'error-empty-object-literal-dictionary'
91+
});
92+
return;
93+
}
94+
95+
// For non-empty object literals, check whether "__proto__: null" is present
96+
const hasNullProto: boolean = node.properties.some(
97+
(prop) =>
98+
prop.type === 'Property' &&
99+
!prop.computed &&
100+
((prop.key.type === 'Identifier' && prop.key.name === '__proto__') ||
101+
(prop.key.type === 'Literal' && prop.key.value === '__proto__')) &&
102+
prop.value.type === 'Literal' &&
103+
prop.value.value === null
104+
);
105+
106+
if (!hasNullProto) {
80107
context.report({
81108
node,
82-
messageId: 'error-object-literal-dictionary'
109+
messageId: 'error-missing-null-prototype'
83110
});
84111
}
85112
}

eslint/eslint-plugin/src/test/null-prototype-dictionaries.test.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,46 @@ ruleTester.run('null-prototype-dictionaries', nullPrototypeDictionariesRule, {
1313
{
1414
// Empty object literal assigned to Record<string, number>
1515
code: 'const dict: Record<string, number> = {};',
16-
errors: [{ messageId: 'error-object-literal-dictionary' }]
16+
errors: [{ messageId: 'error-empty-object-literal-dictionary' }]
1717
},
1818
{
1919
// Empty object literal assigned to index signature type
2020
code: 'const dict: { [key: string]: number } = {};',
21-
errors: [{ messageId: 'error-object-literal-dictionary' }]
22-
},
23-
{
24-
// Non-empty object literal assigned to Record type
25-
code: 'const dict: Record<string, string> = { a: "hello" };',
26-
errors: [{ messageId: 'error-object-literal-dictionary' }]
21+
errors: [{ messageId: 'error-empty-object-literal-dictionary' }]
2722
},
2823
{
2924
// Reassignment to empty object literal
3025
code: [
3126
'let dict: Record<string, number>;',
3227
'dict = {};'
3328
].join('\n'),
34-
errors: [{ messageId: 'error-object-literal-dictionary' }]
29+
errors: [{ messageId: 'error-empty-object-literal-dictionary' }]
3530
},
3631
{
3732
// Return value from function
3833
code: 'function f(): Record<string, number> { return {}; }',
39-
errors: [{ messageId: 'error-object-literal-dictionary' }]
34+
errors: [{ messageId: 'error-empty-object-literal-dictionary' }]
35+
},
36+
{
37+
// Non-empty object literal without __proto__: null
38+
code: 'const dict: Record<string, string> = { a: "hello" };',
39+
errors: [{ messageId: 'error-missing-null-prototype' }]
40+
},
41+
{
42+
// Non-empty object literal with __proto__ set to something other than null
43+
code: 'const dict: Record<string, string> = { __proto__: Object.prototype, a: "hello" };',
44+
errors: [{ messageId: 'error-missing-null-prototype' }]
4045
}
4146
],
4247
valid: [
4348
{
44-
// Correct pattern: Object.create(null) for dictionary
49+
// Correct pattern: Object.create(null) for empty dictionary
4550
code: 'const dict: Record<string, number> = Object.create(null);'
4651
},
52+
{
53+
// Correct pattern: non-empty literal with __proto__: null
54+
code: 'const dict: Record<string, string> = { __proto__: null, a: "hello" };'
55+
},
4756
{
4857
// Regular object type with named properties (not a dictionary)
4958
code: 'const obj: { name: string } = { name: "hello" };'

0 commit comments

Comments
 (0)