Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions test/parallel/test-eslint-avoid-prototype-pollution.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ new RuleTester()
'function name({ [Symbol.match]: m } = kEmptyObject){}',
'function name({ [Symbol.match]: m } = { __proto__: null, [Symbol.match]: 1 }){}',
'function name({ [Symbol.match]: m, c } = { __proto__: null, c: 1 }){}',
'const kNull = ObjectFreeze({ __proto__: null }); ObjectDefineProperty({}, "key", { __proto__: kNull })',
'const kNull = ObjectFreeze({ __proto__: null }); ReflectDefineProperty({}, "key", { __proto__: kNull })',
'const kNull = ObjectFreeze({ "__proto__": null }); ObjectDefineProperties({}, { key: { __proto__: kNull } })',
'const kNull = ObjectFreeze(ObjectCreate(null)); ObjectCreate(null, { key: { __proto__: kNull, value: 1 } })',
'const kNull = ObjectFreeze({ __proto__: null });' +
'function f() { ObjectDefineProperty({}, "k", { __proto__: kNull }) }',
'const kA = ObjectFreeze({ __proto__: null }); const kB = kA; ObjectDefineProperty({}, "k", { __proto__: kB })',
'const kA = ObjectFreeze({ __proto__: null }); const kB = ObjectFreeze({ __proto__: kA });' +
'ObjectDefineProperty({}, "k", { __proto__: kB })',
'const { kEmptyObject } = require("internal/util"); ObjectDefineProperty({}, "k", { __proto__: kEmptyObject })',
'const { kEmptyObject: kE } = require("internal/util"); ObjectDefineProperty({}, "k", { __proto__: kE })',
'const kNull = ObjectFreeze({ __proto__: null }); async function myFn() { return { __proto__: kNull } }',
'const kNull = ObjectFreeze({ __proto__: null }); new Proxy({}, { __proto__: kNull })',
],
invalid: [
{
Expand Down Expand Up @@ -200,6 +213,50 @@ new RuleTester()
code: 'ReflectDefineProperty({}, "key", { enumerable: true })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'ObjectDefineProperty({}, "key", { __proto__: kUnknown })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const kNull = { __proto__: null }; ObjectDefineProperty({}, "key", { __proto__: kNull })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'let kNull = ObjectFreeze({ __proto__: null }); ObjectDefineProperty({}, "key", { __proto__: kNull })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const kObj = ObjectFreeze({}); ObjectDefineProperty({}, "key", { __proto__: kObj })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const kObj = ObjectFreeze({ __proto__: ObjectPrototype });' +
'ObjectDefineProperty({}, "key", { __proto__: kObj })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const kObj = ObjectFreeze(ObjectCreate(ObjectPrototype));' +
'ObjectDefineProperty({}, "key", { __proto__: kObj })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const kNull = ObjectFreeze({ __proto__: null });' +
'function f(kNull) { ObjectDefineProperty({}, "key", { __proto__: kNull }) }',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const { kEmptyObject } = require("internal/other");' +
'ObjectDefineProperty({}, "k", { __proto__: kEmptyObject })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const { other } = require("internal/util"); ObjectDefineProperty({}, "k", { __proto__: other })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'const kObj = ObjectFreeze({}); new Proxy({}, { __proto__: kObj })',
errors: [{ message: /null-prototype/ }],
},
{
code: 'async function myFn(){ return {} }',
errors: [{ message: /null-prototype/ }],
Expand Down
132 changes: 108 additions & 24 deletions tools/eslint-rules/avoid-prototype-pollution.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,121 @@ function checkProperties(context, node) {
}
if (node.type !== 'ObjectExpression') return;
for (const { key, value } of node.properties) {
if (
key != null && value != null &&
!(key.type === 'Identifier' && key.name === '__proto__') &&
!(key.type === 'Literal' && key.value === '__proto__')
) {
if (key != null && value != null && !isProtoKey(key)) {
checkPropertyDescriptor(context, value);
}
}
}

function isNullPrototypeObjectExpression(node) {
if (node.type !== 'ObjectExpression') return;
function isProtoKey(key) {
return key != null &&
((key.type === 'Identifier' && key.name === '__proto__') ||
(key.type === 'Literal' && key.value === '__proto__'));
}

for (const { key, value } of node.properties) {
function findVariable(context, identifier) {
let scope = context.sourceCode.getScope(identifier);
while (scope != null) {
const variable = scope.set.get(identifier.name);
if (variable != null) return variable;
scope = scope.upper;
}
return null;
}

// Frozen null-prototype objects exported from internal modules that can be
// safely used as the prototype of an object literal.
const knownNullPrototypeExports = {
'internal/util': ['kEmptyObject'],
};

function isRequireOfKnownNullPrototypeExport(declarator, name) {
const { id, init } = declarator;
if (
id.type !== 'ObjectPattern' ||
init == null ||
init.type !== 'CallExpression' ||
init.callee.type !== 'Identifier' ||
init.callee.name !== 'require' ||
init.arguments.length !== 1 ||
init.arguments[0].type !== 'Literal'
) {
return false;
}
const allowed = knownNullPrototypeExports[init.arguments[0].value];
if (allowed == null) return false;
return id.properties.some((property) =>
property.type === 'Property' &&
!property.computed &&
property.key.type === 'Identifier' &&
allowed.includes(property.key.name) &&
property.value.type === 'Identifier' &&
property.value.name === name,
);
}

// Returns true if `node` is an expression that is known to evaluate to a
// frozen object whose prototype is null, e.g.:
// const kNullProtoObject = ObjectFreeze({ __proto__: null });
// Using such an object as the `__proto__` of an object literal is equivalent
// to `__proto__: null` in terms of protection against prototype pollution, but
// avoids V8 creating the object literal in dictionary mode.
function isFrozenNullPrototypeObject(context, node, seen = new Set()) {
if (node.type === 'Identifier') {
const variable = findVariable(context, node);
if (variable == null || seen.has(variable) || variable.defs.length !== 1) {
return false;
}
seen.add(variable);
const [def] = variable.defs;
if (
key != null && value != null &&
((key.type === 'Identifier' && key.name === '__proto__') ||
(key.type === 'Literal' && key.value === '__proto__')) &&
value.type === 'Literal' && value.value === null
def.type !== 'Variable' ||
def.parent.kind !== 'const' ||
def.node.init == null
) {
return false;
}
if (def.node.id.type === 'Identifier') {
return isFrozenNullPrototypeObject(context, def.node.init, seen);
}
return isRequireOfKnownNullPrototypeExport(def.node, node.name);
}

if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'ObjectFreeze' &&
node.arguments.length === 1
) {
const [arg] = node.arguments;
if (
arg.type === 'CallExpression' &&
arg.callee.type === 'Identifier' &&
arg.callee.name === 'ObjectCreate' &&
arg.arguments.length === 1 &&
arg.arguments[0].type === 'Literal' &&
arg.arguments[0].value === null
) {
return true;
}
return isNullPrototypeObjectExpression(context, arg, seen);
}

return false;
}

function isNullPrototypeObjectExpression(context, node, seen) {
if (node.type !== 'ObjectExpression') return;

for (const { key, value } of node.properties) {
if (isProtoKey(key) && value != null) {
if (value.type === 'Literal' && value.value === null) {
return true;
}
if (isFrozenNullPrototypeObject(context, value, seen)) {
return true;
}
}
}
return false;
}
Expand All @@ -63,7 +156,7 @@ function checkPropertyDescriptor(context, node) {
}],
});
}
if (isNullPrototypeObjectExpression(node) === false) {
if (isNullPrototypeObjectExpression(context, node) === false) {
context.report({
node,
message: 'Must use null-prototype object for property descriptors',
Expand Down Expand Up @@ -133,7 +226,7 @@ module.exports = {

if (!parent.async) return;
}
if (isNullPrototypeObjectExpression(node) === false) {
if (isNullPrototypeObjectExpression(context, node) === false) {
context.report({
node,
message: 'Use null-prototype when returning from async function',
Expand Down Expand Up @@ -178,16 +271,7 @@ module.exports = {
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.split%', 'Symbol.split'),

'NewExpression[callee.name="Proxy"][arguments.1.type="ObjectExpression"]'(node) {
for (const { key, value } of node.arguments[1].properties) {
if (
key != null && value != null &&
((key.type === 'Identifier' && key.name === '__proto__') ||
(key.type === 'Literal' && key.value === '__proto__')) &&
value.type === 'Literal' && value.value === null
) {
return;
}
}
if (isNullPrototypeObjectExpression(context, node.arguments[1])) return;
context.report({
node,
message: 'Proxy handler must be a null-prototype object',
Expand Down
Loading