From 710288dfddc9343d3990e10a743d49befc5547c8 Mon Sep 17 00:00:00 2001 From: Jared Perreault <90656038+jaredperreault-okta@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:10:20 -0400 Subject: [PATCH] replaces jsonpath-plus module (#1547) OKTA-819401 fix: replaces jsonpath-plus module --- CHANGELOG.md | 6 ++ lib/idx/idxState/v1/idxResponseParser.ts | 2 +- lib/util/jsonpath.ts | 37 ++++++-- package.json | 3 +- test/spec/util/jsonpath.ts | 104 +++++++++++++++++++++-- yarn.lock | 12 +-- 6 files changed, 141 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58fd3f954..7a1aa1852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +# 7.8.1 + +### Bug Fix +- [#1547](https://github.com/okta/okta-auth-js/pull/1547) fix: replaces `jsonpath-plus` module + - Address https://security.snyk.io/vuln/SNYK-JS-JSONPATHPLUS-7945884 + # 7.8.0 ### Features diff --git a/lib/idx/idxState/v1/idxResponseParser.ts b/lib/idx/idxState/v1/idxResponseParser.ts index 1619c5797..86d2e5631 100644 --- a/lib/idx/idxState/v1/idxResponseParser.ts +++ b/lib/idx/idxState/v1/idxResponseParser.ts @@ -78,7 +78,7 @@ const expandRelatesTo = (idxResponse, value) => { if (k === 'relatesTo') { const query = Array.isArray(value[k]) ? value[k][0] : value[k]; if (typeof query === 'string') { - const result = jsonpath({ path: query, json: idxResponse })[0]; + const result = jsonpath({ path: query, json: idxResponse }); if (result) { value[k] = result; return; diff --git a/lib/util/jsonpath.ts b/lib/util/jsonpath.ts index fc1addb54..8a22e98a8 100644 --- a/lib/util/jsonpath.ts +++ b/lib/util/jsonpath.ts @@ -1,8 +1,33 @@ -import { JSONPath, JSONPathOptions } from 'jsonpath-plus'; +const jsonpathRegex = /\$?(?\w+)|(?:\[(?\d+)\])/g; -export function jsonpath(options: JSONPathOptions): any { - // eslint-disable-next-line new-cap - return JSONPath({ - // Disable javascript evaluation by default - preventEval: true, ...options, }); +/* eslint complexity:[0,8] */ +export function jsonpath({ path, json }) { + const steps: string[] = []; + let match: RegExpExecArray | null; + while ((match = jsonpathRegex.exec(path)) !== null) { + const step = match?.groups?.step ?? match?.groups?.index; + if (step) { + steps.push(step); + } + } + + if (steps.length < 1) { + return undefined; + } + + // array length check above guarantees .pop() will return a value + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const lastStep = steps.pop()!; + let curr = json; + for (const step of steps) { + if (Object.prototype.hasOwnProperty.call(curr, step)) { + if (typeof curr[step] !== 'object') { + return undefined; + } + + curr = curr[step]; + } + } + + return curr[lastStep]; } diff --git a/package.json b/package.json index 44bc8f0be..d87a2726f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "@okta/okta-auth-js", "description": "The Okta Auth SDK", - "version": "7.8.0", + "version": "7.8.1", "homepage": "https://github.com/okta/okta-auth-js", "license": "Apache-2.0", "main": "build/cjs/exports/default.js", @@ -159,7 +159,6 @@ "cross-fetch": "^3.1.5", "fast-text-encoding": "^1.0.6", "js-cookie": "^3.0.1", - "jsonpath-plus": "^6.0.1", "node-cache": "^5.1.2", "p-cancelable": "^2.0.0", "tiny-emitter": "1.1.0", diff --git a/test/spec/util/jsonpath.ts b/test/spec/util/jsonpath.ts index dbfa34d05..146a186d0 100644 --- a/test/spec/util/jsonpath.ts +++ b/test/spec/util/jsonpath.ts @@ -1,14 +1,102 @@ import { jsonpath } from '../../../lib/util/jsonpath'; describe('jsonpath', () => { - it('should throw if vulnerable for RCE (remote code execution)', () => { - expect(() => { + + it('should parse json paths from objects', () => { + expect(jsonpath({ + path: '$.foo.bar', + json: { foo: { bar: 'pass' } } + })).toEqual('pass'); + + expect(jsonpath({ + path: 'foo.bar', + json: { foo: { bar: { baz: 'pass' } } } + })).toEqual({ baz: 'pass' }); + + const arrValues1 = Array(30).fill('fail'); + arrValues1[12] = 'pass'; + expect(jsonpath({ + path: '$.foo.bar[12]', + json: { foo: { bar: arrValues1 } } + })).toEqual('pass'); + + const arrValues2 = Array(30).fill({ bar: 'fail' }); + arrValues2[25].bar = 'pass'; + expect(jsonpath({ + path: 'foo[22].bar', + json: { foo: arrValues2 } + })).toEqual('pass'); + + expect(jsonpath({ + path: 'not.a.path', + json: { foo: 1 } + })).toEqual(undefined); + + expect(jsonpath({ + path: '$.foo.bar[12]', + json: { foo: { bar: [] } } + })).toEqual(undefined); + + expect(jsonpath({ + path: '$.foo.bar', + json: { foo: { bar: {} } } + })).toEqual({}); + + expect(jsonpath({ + path: '$.foo.bar.baz', + json: { foo: { bar: [] } } + })).toEqual(undefined); + + expect(jsonpath({ + path: '', + json: { foo: { bar: [] } } + })).toEqual(undefined); + + expect(jsonpath({ + path: '[]', + json: { foo: { bar: [] } } + })).toEqual(undefined); + + expect(jsonpath({ + path: '{}', + json: { foo: { bar: [] } } + })).toEqual(undefined); + }); + + it('should gracefully handle RCE (remote code execution) attempts', () => { + const evalSpy = jest.spyOn(global, 'eval'); + global.alert = jest.fn(); + const alertSpy = jest.spyOn(global, 'alert'); + + expect( + jsonpath({ + path: '$..[?(' + '(function a(arr){' + 'a([...arr, ...arr])' + '})([1]);)]', + json: { + nonEmpty: 'object', + }, + }) + ).toBeUndefined(); + + expect( + jsonpath({ + path: `$[(this.constructor.constructor("eval(alert('foo'))")())]`, + json: { + nonEmpty: 'object', + }, + }) + ).toBeUndefined(); + + expect( jsonpath({ - path: '$..[?(' + '(function a(arr){' + 'a([...arr, ...arr])' + '})([1]);)]', - json: { - nonEmpty: 'object', - }, - }); - }).toThrow(); + path: `$[(this.constructor.constructor("require("child_process").exec("echo 'foo'")")())]`, + json: { + nonEmpty: 'object', + }, + }) + ).toBeUndefined(); + + expect(evalSpy).not.toHaveBeenCalled(); + expect(alertSpy).not.toHaveBeenCalled(); }); + }); diff --git a/yarn.lock b/yarn.lock index 269bf0e22..221f5b899 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3077,16 +3077,16 @@ ansi-red@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-regex@^2.0.0, ansi-regex@^4.1.0, ansi-regex@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== - -ansi-regex@^3.0.1, ansi-regex@^6.0.1: +ansi-regex@^2.0.0, ansi-regex@^3.0.1, ansi-regex@^6.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== +ansi-regex@^4.1.0, ansi-regex@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"