+151
-2
@@ -6,2 +6,148 @@ var aesprim = require('./aesprim'); | ||
| // Property names that must never be accessible in expressions. | ||
| // Mitigates prototype pollution and constructor escape attacks. | ||
| var UNSAFE_PROPERTY_NAMES = Object.create(null); | ||
| /* jshint -W069: true */ | ||
| UNSAFE_PROPERTY_NAMES['constructor'] = true; | ||
| UNSAFE_PROPERTY_NAMES['__proto__'] = true; | ||
| UNSAFE_PROPERTY_NAMES['prototype'] = true; | ||
| /* jshint -W069: false */ | ||
| function isUnsafePropertyName(name) { | ||
| return typeof name === 'string' && UNSAFE_PROPERTY_NAMES[name] === true; | ||
| } | ||
| function isSafeAst(ast) { | ||
| if (!ast || typeof ast !== 'object') return false; | ||
| function walk(node) { | ||
| if (!node || typeof node !== 'object' || !node.type) { | ||
| return false; | ||
| } | ||
| switch (node.type) { | ||
| // ===== SAFE TERMINALS ===== | ||
| case 'Literal': | ||
| return true; | ||
| case 'Identifier': | ||
| // Only allow the special scope identifier | ||
| return node.name === '@'; | ||
| // ===== PROPERTY ACCESS ===== | ||
| case 'MemberExpression': { | ||
| if (!walk(node.object)) { | ||
| return false; | ||
| } | ||
| // Non-computed: obj.property | ||
| if (!node.computed && node.property.type === 'Identifier') { | ||
| if (isUnsafePropertyName(node.property.name)) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| // Computed: obj["property"] | ||
| if (node.computed) { | ||
| if (!walk(node.property)) { | ||
| return false; | ||
| } | ||
| if ( | ||
| node.property.type === 'Literal' && | ||
| isUnsafePropertyName(String(node.property.value)) | ||
| ) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| // ===== EXPRESSIONS ===== | ||
| case 'UnaryExpression': | ||
| return walk(node.argument); | ||
| case 'BinaryExpression': | ||
| case 'LogicalExpression': | ||
| return walk(node.left) && walk(node.right); | ||
| case 'ConditionalExpression': | ||
| return ( | ||
| walk(node.test) && | ||
| walk(node.consequent) && | ||
| walk(node.alternate) | ||
| ); | ||
| case 'ArrayExpression': | ||
| for (var i = 0; i < node.elements.length; i++) { | ||
| if (!walk(node.elements[i])) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| case 'ObjectExpression': | ||
| for (var j = 0; j < node.properties.length; j++) { | ||
| var prop = node.properties[j]; | ||
| // Reject unsafe keys | ||
| if ( | ||
| prop.key && | ||
| ( | ||
| (prop.key.type === 'Identifier' && | ||
| isUnsafePropertyName(prop.key.name)) || | ||
| (prop.key.type === 'Literal' && | ||
| isUnsafePropertyName(String(prop.key.value))) | ||
| ) | ||
| ) { | ||
| return false; | ||
| } | ||
| if (!walk(prop.value)) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| // ===== EXPLICITLY REJECT DANGEROUS TYPES ===== | ||
| // Security: do not rely on default deny; list each code-execution / escape vector. | ||
| case 'CallExpression': | ||
| case 'NewExpression': | ||
| case 'FunctionExpression': | ||
| case 'ArrowFunctionExpression': | ||
| case 'ThisExpression': | ||
| case 'AssignmentExpression': | ||
| case 'UpdateExpression': | ||
| case 'SequenceExpression': | ||
| case 'TemplateLiteral': | ||
| case 'TemplateElement': | ||
| case 'TaggedTemplateExpression': | ||
| case 'ReturnStatement': | ||
| case 'ExpressionStatement': | ||
| return false; | ||
| // ===== DEFAULT DENY ===== | ||
| default: | ||
| return false; | ||
| } | ||
| } | ||
| return walk(ast); | ||
| } | ||
| var Handlers = function() { | ||
@@ -243,4 +389,7 @@ return this.initialize.apply(this, arguments); | ||
| function evaluate() { | ||
| try { return _evaluate.apply(this, arguments) } | ||
| function evaluate(ast, scope) { | ||
| if (!isSafeAst(ast)) { | ||
| throw new Error('Unsafe expression: script and filter expressions may only access the current node (@) with safe property names'); | ||
| } | ||
| try { return _evaluate(ast, scope) } | ||
| catch (e) { } | ||
@@ -247,0 +396,0 @@ } |
+1
-1
| { | ||
| "name": "jsonpath", | ||
| "description": "Query JavaScript objects with JSONPath expressions. Robust / safe JSONPath engine for Node.js.", | ||
| "version": "1.2.1", | ||
| "version": "1.3.0", | ||
| "author": "david@fmail.co.uk", | ||
@@ -6,0 +6,0 @@ "scripts": { |
+187
-0
@@ -51,2 +51,189 @@ var assert = require('assert'); | ||
| }); | ||
| suite('CVE-2026-1615: blocks code injection in filter/script expressions', function() { | ||
| var data = { a: {}, b: [1, 2, 3] }; | ||
| test('rejects constructor access in filter expression', function() { | ||
| assert.throws(function() { | ||
| jp.query(data, '$[?(@.constructor)]'); | ||
| }, /Unsafe expression/); | ||
| }); | ||
| test('rejects constructor.constructor in filter expression', function() { | ||
| assert.throws(function() { | ||
| jp.query(data, '$[?(@.constructor.constructor)]'); | ||
| }, /Unsafe expression/); | ||
| }); | ||
| test('rejects chained constructor.constructor call: @.foo["constructor"]["constructor"](...)()', function() { | ||
| assert.throws(function() { | ||
| jp.query(data, '$[?(@.foo["constructor"]["constructor"]("return process")())]'); | ||
| }, /Unsafe expression/); | ||
| }); | ||
| test('rejects __proto__ access in filter expression', function() { | ||
| assert.throws(function() { | ||
| jp.query(data, '$[?(@.__proto__)]'); | ||
| }, /Unsafe expression/); | ||
| }); | ||
| test('rejects function call in filter expression', function() { | ||
| assert.throws(function() { | ||
| jp.query(data, '$[?(process.exit(1))]'); | ||
| }, /Unsafe expression/); | ||
| }); | ||
| test('rejects constructor access in script expression', function() { | ||
| var scriptData = { a: [1, 2, 3] }; | ||
| assert.throws(function() { | ||
| jp.query(scriptData, '$[(@.constructor)]'); | ||
| }, /Unsafe expression/); | ||
| }); | ||
| test('allows safe filter expressions', function() { | ||
| var storeData = { store: { book: [ { price: 5 }, { price: 15 } ] } }; | ||
| var results = jp.query(storeData, '$..book[?(@.price<10)]'); | ||
| assert.deepEqual(results, [ { price: 5 } ]); | ||
| }); | ||
| test('allows safe script expressions', function() { | ||
| var bookData = { book: [ { id: 1 }, { id: 2 }, { id: 3 } ] }; | ||
| var results = jp.nodes(bookData, '$..book[(@.length-1)]'); | ||
| assert.deepEqual(results[0].value, { id: 3 }); | ||
| }); | ||
| test('rejects bracket notation constructor: @["constructor"]', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@["constructor"])]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects bracket notation __proto__: @["__proto__"]', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@["__proto__"])]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects bracket notation prototype: @["prototype"]', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@["prototype"])]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects ObjectExpression with unsafe key: { "__proto__": @ }', function() { | ||
| assert.throws(function() { jp.query(data, '$[?({ "__proto__": @ })]'); }, /Unsafe expression|Unexpected token/); | ||
| }); | ||
| test('rejects ObjectExpression with unsafe key: { "constructor": @ }', function() { | ||
| assert.throws(function() { jp.query(data, '$[?({ "constructor": @ })]'); }, /Unsafe expression|Unexpected token/); | ||
| }); | ||
| test('rejects ObjectExpression with unsafe key: { "prototype": @ }', function() { | ||
| assert.throws(function() { jp.query(data, '$[?({ "prototype": @ })]'); }, /Unsafe expression|Unexpected token/); | ||
| }); | ||
| test('rejects unicode escape constructor in bracket: @["\\u0063onstructor"]', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@["\\u0063onstructor"])]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects unicode escape __proto__ in bracket', function() { | ||
| var path = '$[?(@["\\u005f\\u005fproto\\u005f\\u005f"])]'; | ||
| assert.throws(function() { jp.query(data, path); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects IIFE: (function(){return 1})()', function() { | ||
| assert.throws(function() { jp.query(data, '$[?((function(){return 1})())]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects direct function call: process.exit(1)', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(process.exit(1))]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects require() call', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(require("fs"))]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects eval() call', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(eval("1"))]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects globalThis / global identifier', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(globalThis)]'); }, /Unsafe expression/); | ||
| assert.throws(function() { jp.query(data, '$[?(global)]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects NewExpression: new Function("return 1")()', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(new Function("return 1")())]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects JSFuck-style: [] ["filter"]["constructor"]', function() { | ||
| assert.throws(function() { jp.query(data, '$[?([]["filter"]["constructor"])]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects JSFuck-style constructor call (no @)', function() { | ||
| assert.throws(function() { jp.query(data, '$[?([]["filter"]["constructor"]("return 1")())]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects sequence expression: (1, process.exit)(1)', function() { | ||
| assert.throws(function() { jp.query(data, '$[?((1, process.exit)(1))]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects method call on @: @.valueOf()', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@.valueOf())]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects method call: @.toString()', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@.toString())]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects template literal in computed: @[`constructor`]', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@[`constructor`])]'); }, /Unsafe expression|Unexpected token|ILLEGAL/); | ||
| }); | ||
| test('rejects tagged template (code execution vector)', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(String.raw`x`)]'); }, /Unsafe expression|Unexpected token|ILLEGAL/); | ||
| }); | ||
| test('rejects ArrowFunctionExpression', function() { | ||
| assert.throws(function() { jp.query(data, '$[?((()=>1)())]'); }, /Unsafe expression|Unexpected token/); | ||
| }); | ||
| test('rejects ThisExpression (this)', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(this)]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects script expression with constructor', function() { | ||
| assert.throws(function() { jp.query(data, '$[(@.constructor)]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects script expression with call', function() { | ||
| assert.throws(function() { jp.query(data, '$[((function(){return 0})())]'); }, /Unsafe expression/); | ||
| }); | ||
| test('allows @.length (no call)', function() { | ||
| var r = jp.query(data, '$[?(@.length)]'); | ||
| assert.ok(Array.isArray(r)); | ||
| }); | ||
| test('allows bracket with safe key: @["length"]', function() { | ||
| var r = jp.query(data, '$[?(@["length"])]'); | ||
| assert.ok(Array.isArray(r)); | ||
| }); | ||
| test('allows @["@class"] (existing test pattern)', function() { | ||
| var d = { DIV: [{ '@class': 'value', val: 5 }] }; | ||
| var r = jp.query(d, '$..DIV[?(@["@class"]=="value")]'); | ||
| assert.deepEqual(r, d.DIV); | ||
| }); | ||
| test('rejects prototype access in filter', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@.prototype)]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects comma/sequence that could hide call', function() { | ||
| assert.throws(function() { jp.query(data, '$[?((0, eval)("1"))]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects AssignmentExpression', function() { | ||
| assert.throws(function() { jp.query(data, '$[?((x=1)==1)]'); }, /Unsafe expression/); | ||
| }); | ||
| test('rejects UpdateExpression (++, --)', function() { | ||
| assert.throws(function() { jp.query(data, '$[?(@.x++)]'); }, /Unsafe expression/); | ||
| }); | ||
| }); | ||
| }); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
427712
4.08%8585
4.61%6
200%