| 1 | var assert = require('assert');
|
|---|
| 2 | var jp = require('../');
|
|---|
| 3 |
|
|---|
| 4 | suite('security', function() {
|
|---|
| 5 |
|
|---|
| 6 | var cleanup = function() {
|
|---|
| 7 | if (Object.prototype.polluted) {
|
|---|
| 8 | delete Object.prototype.polluted;
|
|---|
| 9 | }
|
|---|
| 10 | };
|
|---|
| 11 |
|
|---|
| 12 | teardown(function() {
|
|---|
| 13 | cleanup();
|
|---|
| 14 | });
|
|---|
| 15 |
|
|---|
| 16 | test('blocks prototype pollution via value()', function() {
|
|---|
| 17 | cleanup();
|
|---|
| 18 | var data = {};
|
|---|
| 19 | assert.throws(function() {
|
|---|
| 20 | jp.value(data, '$.__proto__.polluted', 'yes');
|
|---|
| 21 | }, /Unsafe key/);
|
|---|
| 22 | assert.equal(({}).polluted, undefined);
|
|---|
| 23 | });
|
|---|
| 24 |
|
|---|
| 25 | test('blocks prototype pollution via apply()', function() {
|
|---|
| 26 | cleanup();
|
|---|
| 27 | var data = { safe: { ok: true } };
|
|---|
| 28 | assert.throws(function() {
|
|---|
| 29 | jp.apply(data, '$.__proto__.polluted', function() { return 'yes'; });
|
|---|
| 30 | }, /Unsafe key/);
|
|---|
| 31 | assert.equal(({}).polluted, undefined);
|
|---|
| 32 | });
|
|---|
| 33 |
|
|---|
| 34 | test('blocks unsafe subscript access', function() {
|
|---|
| 35 | cleanup();
|
|---|
| 36 | var data = {};
|
|---|
| 37 | assert.throws(function() {
|
|---|
| 38 | jp.query(data, '$["__proto__"]["polluted"]');
|
|---|
| 39 | }, /Unsafe key/);
|
|---|
| 40 | assert.equal(({}).polluted, undefined);
|
|---|
| 41 | });
|
|---|
| 42 |
|
|---|
| 43 | test('blocks unsafe union access', function() {
|
|---|
| 44 | cleanup();
|
|---|
| 45 | var data = { safe: 1 };
|
|---|
| 46 | assert.throws(function() {
|
|---|
| 47 | jp.nodes(data, "$['safe','__proto__']");
|
|---|
| 48 | }, /Unsafe key/);
|
|---|
| 49 | assert.equal(({}).polluted, undefined);
|
|---|
| 50 | });
|
|---|
| 51 |
|
|---|
| 52 | suite('CVE-2026-1615: blocks code injection in filter/script expressions', function() {
|
|---|
| 53 | var data = { a: {}, b: [1, 2, 3] };
|
|---|
| 54 |
|
|---|
| 55 | test('rejects constructor access in filter expression', function() {
|
|---|
| 56 | assert.throws(function() {
|
|---|
| 57 | jp.query(data, '$[?(@.constructor)]');
|
|---|
| 58 | }, /Unsafe expression/);
|
|---|
| 59 | });
|
|---|
| 60 |
|
|---|
| 61 | test('rejects constructor.constructor in filter expression', function() {
|
|---|
| 62 | assert.throws(function() {
|
|---|
| 63 | jp.query(data, '$[?(@.constructor.constructor)]');
|
|---|
| 64 | }, /Unsafe expression/);
|
|---|
| 65 | });
|
|---|
| 66 |
|
|---|
| 67 | test('rejects chained constructor.constructor call: @.foo["constructor"]["constructor"](...)()', function() {
|
|---|
| 68 | assert.throws(function() {
|
|---|
| 69 | jp.query(data, '$[?(@.foo["constructor"]["constructor"]("return process")())]');
|
|---|
| 70 | }, /Unsafe expression/);
|
|---|
| 71 | });
|
|---|
| 72 |
|
|---|
| 73 | test('rejects __proto__ access in filter expression', function() {
|
|---|
| 74 | assert.throws(function() {
|
|---|
| 75 | jp.query(data, '$[?(@.__proto__)]');
|
|---|
| 76 | }, /Unsafe expression/);
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | test('rejects function call in filter expression', function() {
|
|---|
| 80 | assert.throws(function() {
|
|---|
| 81 | jp.query(data, '$[?(process.exit(1))]');
|
|---|
| 82 | }, /Unsafe expression/);
|
|---|
| 83 | });
|
|---|
| 84 |
|
|---|
| 85 | test('rejects constructor access in script expression', function() {
|
|---|
| 86 | var scriptData = { a: [1, 2, 3] };
|
|---|
| 87 | assert.throws(function() {
|
|---|
| 88 | jp.query(scriptData, '$[(@.constructor)]');
|
|---|
| 89 | }, /Unsafe expression/);
|
|---|
| 90 | });
|
|---|
| 91 |
|
|---|
| 92 | test('allows safe filter expressions', function() {
|
|---|
| 93 | var storeData = { store: { book: [ { price: 5 }, { price: 15 } ] } };
|
|---|
| 94 | var results = jp.query(storeData, '$..book[?(@.price<10)]');
|
|---|
| 95 | assert.deepEqual(results, [ { price: 5 } ]);
|
|---|
| 96 | });
|
|---|
| 97 |
|
|---|
| 98 | test('allows safe script expressions', function() {
|
|---|
| 99 | var bookData = { book: [ { id: 1 }, { id: 2 }, { id: 3 } ] };
|
|---|
| 100 | var results = jp.nodes(bookData, '$..book[(@.length-1)]');
|
|---|
| 101 | assert.deepEqual(results[0].value, { id: 3 });
|
|---|
| 102 | });
|
|---|
| 103 |
|
|---|
| 104 | test('rejects bracket notation constructor: @["constructor"]', function() {
|
|---|
| 105 | assert.throws(function() { jp.query(data, '$[?(@["constructor"])]'); }, /Unsafe expression/);
|
|---|
| 106 | });
|
|---|
| 107 |
|
|---|
| 108 | test('rejects bracket notation __proto__: @["__proto__"]', function() {
|
|---|
| 109 | assert.throws(function() { jp.query(data, '$[?(@["__proto__"])]'); }, /Unsafe expression/);
|
|---|
| 110 | });
|
|---|
| 111 |
|
|---|
| 112 | test('rejects bracket notation prototype: @["prototype"]', function() {
|
|---|
| 113 | assert.throws(function() { jp.query(data, '$[?(@["prototype"])]'); }, /Unsafe expression/);
|
|---|
| 114 | });
|
|---|
| 115 |
|
|---|
| 116 | test('rejects ObjectExpression with unsafe key: { "__proto__": @ }', function() {
|
|---|
| 117 | assert.throws(function() { jp.query(data, '$[?({ "__proto__": @ })]'); }, /Unsafe expression|Unexpected token/);
|
|---|
| 118 | });
|
|---|
| 119 |
|
|---|
| 120 | test('rejects ObjectExpression with unsafe key: { "constructor": @ }', function() {
|
|---|
| 121 | assert.throws(function() { jp.query(data, '$[?({ "constructor": @ })]'); }, /Unsafe expression|Unexpected token/);
|
|---|
| 122 | });
|
|---|
| 123 |
|
|---|
| 124 | test('rejects ObjectExpression with unsafe key: { "prototype": @ }', function() {
|
|---|
| 125 | assert.throws(function() { jp.query(data, '$[?({ "prototype": @ })]'); }, /Unsafe expression|Unexpected token/);
|
|---|
| 126 | });
|
|---|
| 127 |
|
|---|
| 128 | test('rejects unicode escape constructor in bracket: @["\\u0063onstructor"]', function() {
|
|---|
| 129 | assert.throws(function() { jp.query(data, '$[?(@["\\u0063onstructor"])]'); }, /Unsafe expression/);
|
|---|
| 130 | });
|
|---|
| 131 |
|
|---|
| 132 | test('rejects unicode escape __proto__ in bracket', function() {
|
|---|
| 133 | var path = '$[?(@["\\u005f\\u005fproto\\u005f\\u005f"])]';
|
|---|
| 134 | assert.throws(function() { jp.query(data, path); }, /Unsafe expression/);
|
|---|
| 135 | });
|
|---|
| 136 |
|
|---|
| 137 | test('rejects IIFE: (function(){return 1})()', function() {
|
|---|
| 138 | assert.throws(function() { jp.query(data, '$[?((function(){return 1})())]'); }, /Unsafe expression/);
|
|---|
| 139 | });
|
|---|
| 140 |
|
|---|
| 141 | test('rejects direct function call: process.exit(1)', function() {
|
|---|
| 142 | assert.throws(function() { jp.query(data, '$[?(process.exit(1))]'); }, /Unsafe expression/);
|
|---|
| 143 | });
|
|---|
| 144 |
|
|---|
| 145 | test('rejects require() call', function() {
|
|---|
| 146 | assert.throws(function() { jp.query(data, '$[?(require("fs"))]'); }, /Unsafe expression/);
|
|---|
| 147 | });
|
|---|
| 148 |
|
|---|
| 149 | test('rejects eval() call', function() {
|
|---|
| 150 | assert.throws(function() { jp.query(data, '$[?(eval("1"))]'); }, /Unsafe expression/);
|
|---|
| 151 | });
|
|---|
| 152 |
|
|---|
| 153 | test('rejects globalThis / global identifier', function() {
|
|---|
| 154 | assert.throws(function() { jp.query(data, '$[?(globalThis)]'); }, /Unsafe expression/);
|
|---|
| 155 | assert.throws(function() { jp.query(data, '$[?(global)]'); }, /Unsafe expression/);
|
|---|
| 156 | });
|
|---|
| 157 |
|
|---|
| 158 | test('rejects NewExpression: new Function("return 1")()', function() {
|
|---|
| 159 | assert.throws(function() { jp.query(data, '$[?(new Function("return 1")())]'); }, /Unsafe expression/);
|
|---|
| 160 | });
|
|---|
| 161 |
|
|---|
| 162 | test('rejects JSFuck-style: [] ["filter"]["constructor"]', function() {
|
|---|
| 163 | assert.throws(function() { jp.query(data, '$[?([]["filter"]["constructor"])]'); }, /Unsafe expression/);
|
|---|
| 164 | });
|
|---|
| 165 |
|
|---|
| 166 | test('rejects JSFuck-style constructor call (no @)', function() {
|
|---|
| 167 | assert.throws(function() { jp.query(data, '$[?([]["filter"]["constructor"]("return 1")())]'); }, /Unsafe expression/);
|
|---|
| 168 | });
|
|---|
| 169 |
|
|---|
| 170 | test('rejects sequence expression: (1, process.exit)(1)', function() {
|
|---|
| 171 | assert.throws(function() { jp.query(data, '$[?((1, process.exit)(1))]'); }, /Unsafe expression/);
|
|---|
| 172 | });
|
|---|
| 173 |
|
|---|
| 174 | test('rejects method call on @: @.valueOf()', function() {
|
|---|
| 175 | assert.throws(function() { jp.query(data, '$[?(@.valueOf())]'); }, /Unsafe expression/);
|
|---|
| 176 | });
|
|---|
| 177 |
|
|---|
| 178 | test('rejects method call: @.toString()', function() {
|
|---|
| 179 | assert.throws(function() { jp.query(data, '$[?(@.toString())]'); }, /Unsafe expression/);
|
|---|
| 180 | });
|
|---|
| 181 |
|
|---|
| 182 | test('rejects template literal in computed: @[`constructor`]', function() {
|
|---|
| 183 | assert.throws(function() { jp.query(data, '$[?(@[`constructor`])]'); }, /Unsafe expression|Unexpected token|ILLEGAL/);
|
|---|
| 184 | });
|
|---|
| 185 |
|
|---|
| 186 | test('rejects tagged template (code execution vector)', function() {
|
|---|
| 187 | assert.throws(function() { jp.query(data, '$[?(String.raw`x`)]'); }, /Unsafe expression|Unexpected token|ILLEGAL/);
|
|---|
| 188 | });
|
|---|
| 189 |
|
|---|
| 190 | test('rejects ArrowFunctionExpression', function() {
|
|---|
| 191 | assert.throws(function() { jp.query(data, '$[?((()=>1)())]'); }, /Unsafe expression|Unexpected token/);
|
|---|
| 192 | });
|
|---|
| 193 |
|
|---|
| 194 | test('rejects ThisExpression (this)', function() {
|
|---|
| 195 | assert.throws(function() { jp.query(data, '$[?(this)]'); }, /Unsafe expression/);
|
|---|
| 196 | });
|
|---|
| 197 |
|
|---|
| 198 | test('rejects script expression with constructor', function() {
|
|---|
| 199 | assert.throws(function() { jp.query(data, '$[(@.constructor)]'); }, /Unsafe expression/);
|
|---|
| 200 | });
|
|---|
| 201 |
|
|---|
| 202 | test('rejects script expression with call', function() {
|
|---|
| 203 | assert.throws(function() { jp.query(data, '$[((function(){return 0})())]'); }, /Unsafe expression/);
|
|---|
| 204 | });
|
|---|
| 205 |
|
|---|
| 206 | test('allows @.length (no call)', function() {
|
|---|
| 207 | var r = jp.query(data, '$[?(@.length)]');
|
|---|
| 208 | assert.ok(Array.isArray(r));
|
|---|
| 209 | });
|
|---|
| 210 |
|
|---|
| 211 | test('allows bracket with safe key: @["length"]', function() {
|
|---|
| 212 | var r = jp.query(data, '$[?(@["length"])]');
|
|---|
| 213 | assert.ok(Array.isArray(r));
|
|---|
| 214 | });
|
|---|
| 215 |
|
|---|
| 216 | test('allows @["@class"] (existing test pattern)', function() {
|
|---|
| 217 | var d = { DIV: [{ '@class': 'value', val: 5 }] };
|
|---|
| 218 | var r = jp.query(d, '$..DIV[?(@["@class"]=="value")]');
|
|---|
| 219 | assert.deepEqual(r, d.DIV);
|
|---|
| 220 | });
|
|---|
| 221 |
|
|---|
| 222 | test('rejects prototype access in filter', function() {
|
|---|
| 223 | assert.throws(function() { jp.query(data, '$[?(@.prototype)]'); }, /Unsafe expression/);
|
|---|
| 224 | });
|
|---|
| 225 |
|
|---|
| 226 | test('rejects comma/sequence that could hide call', function() {
|
|---|
| 227 | assert.throws(function() { jp.query(data, '$[?((0, eval)("1"))]'); }, /Unsafe expression/);
|
|---|
| 228 | });
|
|---|
| 229 |
|
|---|
| 230 | test('rejects AssignmentExpression', function() {
|
|---|
| 231 | assert.throws(function() { jp.query(data, '$[?((x=1)==1)]'); }, /Unsafe expression/);
|
|---|
| 232 | });
|
|---|
| 233 |
|
|---|
| 234 | test('rejects UpdateExpression (++, --)', function() {
|
|---|
| 235 | assert.throws(function() { jp.query(data, '$[?(@.x++)]'); }, /Unsafe expression/);
|
|---|
| 236 | });
|
|---|
| 237 | });
|
|---|
| 238 | });
|
|---|