├── .airtap.yml ├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── benchmarks ├── .npmrc ├── ignore.js ├── no__proto__.js ├── package.json ├── remove.js ├── throw.js └── valid.js ├── eslint.config.js ├── index.js ├── package.json ├── test └── index.test.js └── types ├── index.d.ts └── index.test-d.ts /.airtap.yml: -------------------------------------------------------------------------------- 1 | providers: 2 | - airtap-playwright 3 | 4 | browsers: 5 | - name: chromium 6 | supports: 7 | headless: true 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | dependency-review: 22 | name: Dependency Review 23 | if: github.event_name == 'pull_request' 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | steps: 28 | - name: Check out repo 29 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 30 | with: 31 | persist-credentials: false 32 | 33 | - name: Dependency review 34 | uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0 35 | 36 | lint: 37 | name: Lint Code 38 | runs-on: ubuntu-latest 39 | permissions: 40 | contents: read 41 | steps: 42 | - name: Check out repo 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | with: 45 | persist-credentials: false 46 | 47 | - name: Setup Node 48 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 49 | with: 50 | check-latest: true 51 | node-version: lts/* 52 | 53 | - name: Install dependencies 54 | run: npm i --ignore-scripts 55 | 56 | - name: Lint code 57 | run: npm run lint 58 | 59 | browsers: 60 | name: Test Browsers 61 | runs-on: ubuntu-latest 62 | permissions: 63 | contents: read 64 | steps: 65 | - name: Check out repo 66 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | with: 68 | persist-credentials: false 69 | 70 | - name: Setup Node 71 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 72 | with: 73 | check-latest: true 74 | node-version: lts/* 75 | 76 | - name: Install dependencies 77 | run: npm i 78 | 79 | - name: Install Playwright 80 | run: npx playwright install 81 | 82 | - name: Run tests 83 | run: npm run test:browser 84 | 85 | test: 86 | name: Test 87 | runs-on: ubuntu-latest 88 | permissions: 89 | contents: read 90 | strategy: 91 | matrix: 92 | node-version: [20, 22, 24] 93 | steps: 94 | - name: Check out repo 95 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 96 | with: 97 | persist-credentials: false 98 | 99 | - name: Setup Node ${{ matrix.node-version }} 100 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 101 | with: 102 | check-latest: true 103 | node-version: ${{ matrix.node-version }} 104 | 105 | - name: Install dependencies 106 | run: npm i --ignore-scripts 107 | 108 | - name: Run tests 109 | run: npm run test:unit 110 | 111 | typescript: 112 | name: Test TypeScript 113 | runs-on: ubuntu-latest 114 | permissions: 115 | contents: read 116 | steps: 117 | - name: Check out repo 118 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 119 | with: 120 | persist-credentials: false 121 | 122 | - name: Setup Node 123 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 124 | with: 125 | check-latest: true 126 | node-version: lts/* 127 | 128 | - name: Install dependencies 129 | run: npm i --ignore-scripts 130 | 131 | - name: tsd 132 | run: npm run test:typescript 133 | 134 | automerge: 135 | name: Automerge Dependabot PRs 136 | if: > 137 | github.event_name == 'pull_request' && 138 | github.event.pull_request.user.login == 'dependabot[bot]' 139 | needs: [browsers, lint, test, typescript] 140 | permissions: 141 | pull-requests: write 142 | contents: write 143 | runs-on: ubuntu-latest 144 | steps: 145 | - uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 146 | with: 147 | github-token: ${{ secrets.GITHUB_TOKEN }} 148 | target: major 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | 154 | **/._* 155 | **/*.pem 156 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 The Fastify Team 2 | Copyright (c) 2019, Sideway Inc, and project contributors 3 | All rights reserved. 4 | 5 | The complete list of contributors can be found at: 6 | - https://github.com/hapijs/bourne/graphs/contributors 7 | - https://github.com/fastify/secure-json-parse/graphs/contributors 8 | 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secure-json-parse 2 | 3 | [![CI](https://github.com/fastify/secure-json-parse/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/secure-json-parse/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/secure-json-parse.svg?style=flat)](https://www.npmjs.com/package/secure-json-parse) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | `JSON.parse()` drop-in replacement with prototype poisoning protection. 8 | 9 | ## Introduction 10 | 11 | Consider this: 12 | 13 | ```js 14 | > const a = '{"__proto__":{ "b":5}}'; 15 | '{"__proto__":{ "b":5}}' 16 | 17 | > const b = JSON.parse(a); 18 | { __proto__: { b: 5 } } 19 | 20 | > b.b; 21 | undefined 22 | 23 | > const c = Object.assign({}, b); 24 | {} 25 | 26 | > c.b 27 | 5 28 | ``` 29 | 30 | The problem is that `JSON.parse()` retains the `__proto__` property as a plain object key. By 31 | itself, this is not a security issue. However, as soon as that object is assigned to another or 32 | iterated on and values copied, the `__proto__` property leaks and becomes the object's prototype. 33 | 34 | ## Install 35 | ``` 36 | npm i secure-json-parse 37 | ``` 38 | 39 | ## Usage 40 | 41 | Pass the option object as a second (or third) parameter for configuring the action to take in case of a bad JSON, if nothing is configured, the default is to throw a `SyntaxError`.
42 | You can choose which action to perform in case `__proto__` is present, and in case `constructor.prototype` is present. 43 | 44 | ```js 45 | const sjson = require('secure-json-parse') 46 | 47 | const goodJson = '{ "a": 5, "b": 6 }' 48 | const badJson = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "constructor": {"prototype": {"bar": "baz"} } }' 49 | 50 | console.log(JSON.parse(goodJson), sjson.parse(goodJson, undefined, { protoAction: 'remove', constructorAction: 'remove' })) 51 | console.log(JSON.parse(badJson), sjson.parse(badJson, undefined, { protoAction: 'remove', constructorAction: 'remove' })) 52 | ``` 53 | 54 | ## API 55 | 56 | ### `sjson.parse(text, [reviver], [options])` 57 | 58 | Parses a given JSON-formatted text into an object where: 59 | - `text` - the JSON text string. 60 | - `reviver` - the `JSON.parse()` optional `reviver` argument. 61 | - `options` - optional configuration object where: 62 | - `protoAction` - optional string with one of: 63 | - `'error'` - throw a `SyntaxError` when a `__proto__` key is found. This is the default value. 64 | - `'remove'` - deletes any `__proto__` keys from the result object. 65 | - `'ignore'` - skips all validation (same as calling `JSON.parse()` directly). 66 | - `constructorAction` - optional string with one of: 67 | - `'error'` - throw a `SyntaxError` when a `constructor.prototype` key is found. This is the default value. 68 | - `'remove'` - deletes any `constructor` keys from the result object. 69 | - `'ignore'` - skips all validation (same as calling `JSON.parse()` directly). 70 | 71 | ### `sjson.scan(obj, [options])` 72 | 73 | Scans a given object for prototype properties where: 74 | - `obj` - the object being scanned. 75 | - `options` - optional configuration object where: 76 | - `protoAction` - optional string with one of: 77 | - `'error'` - throw a `SyntaxError` when a `__proto__` key is found. This is the default value. 78 | - `'remove'` - deletes any `__proto__` keys from the input `obj`. 79 | - `constructorAction` - optional string with one of: 80 | - `'error'` - throw a `SyntaxError` when a `constructor.prototype` key is found. This is the default value. 81 | - `'remove'` - deletes any `constructor` keys from the input `obj`. 82 | 83 | ## Benchmarks 84 | 85 | Machine: 2,7 GHz Quad-Core Intel Core i7 86 | 87 | ``` 88 | v14.8.0 89 | 90 | > node ignore.js 91 | 92 | JSON.parse x 679,376 ops/sec ±1.15% (84 runs sampled) 93 | secure-json-parse x 649,605 ops/sec ±0.58% (87 runs sampled) 94 | reviver x 244,414 ops/sec ±1.05% (88 runs sampled) 95 | Fastest is JSON.parse 96 | 97 | > node no__proto__.js 98 | 99 | JSON.parse x 652,190 ops/sec ±0.67% (86 runs sampled) 100 | secure-json-parse x 589,785 ops/sec ±1.01% (88 runs sampled) 101 | reviver x 218,075 ops/sec ±1.58% (87 runs sampled) 102 | Fastest is JSON.parse 103 | 104 | > node remove.js 105 | 106 | JSON.parse x 683,527 ops/sec ±0.62% (88 runs sampled) 107 | secure-json-parse x 316,926 ops/sec ±0.63% (87 runs sampled) 108 | reviver x 214,167 ops/sec ±0.63% (86 runs sampled) 109 | Fastest is JSON.parse 110 | 111 | > node throw.js 112 | 113 | JSON.parse x 682,548 ops/sec ±0.60% (88 runs sampled) 114 | JSON.parse error x 170,716 ops/sec ±0.93% (87 runs sampled) 115 | secure-json-parse x 104,483 ops/sec ±0.62% (87 runs sampled) 116 | reviver x 114,197 ops/sec ±0.63% (87 runs sampled) 117 | Fastest is JSON.parse 118 | ``` 119 | 120 | ## Acknowledgments 121 | This project has been forked from [hapijs/bourne](https://github.com/hapijs/bourne). 122 | All credit before commit [4690682](https://github.com/hapijs/bourne/commit/4690682c6cdaa06590da7b2485d5df91c09da889) goes to the hapijs/bourne project contributors. 123 | After, the project will be maintained by the Fastify team. 124 | 125 | ## License 126 | Licensed under [BSD-3-Clause](./LICENSE). 127 | -------------------------------------------------------------------------------- /benchmarks/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /benchmarks/ignore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const sjson = require('..') 5 | 6 | const internals = { 7 | text: '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 8 | } 9 | 10 | const suite = new Benchmark.Suite() 11 | 12 | suite 13 | .add('JSON.parse', () => { 14 | JSON.parse(internals.text) 15 | }) 16 | .add('secure-json-parse parse', () => { 17 | sjson.parse(internals.text, { protoAction: 'ignore' }) 18 | }) 19 | .add('secure-json-parse safeParse', () => { 20 | sjson.safeParse(internals.text) 21 | }) 22 | .add('reviver', () => { 23 | JSON.parse(internals.text, internals.reviver) 24 | }) 25 | .on('cycle', (event) => { 26 | console.log(String(event.target)) 27 | }) 28 | .on('complete', function () { 29 | console.log('Fastest is ' + this.filter('fastest').map('name')) 30 | }) 31 | .run({ async: true }) 32 | 33 | internals.reviver = function (_key, value) { 34 | return value 35 | } 36 | -------------------------------------------------------------------------------- /benchmarks/no__proto__.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const sjson = require('..') 5 | 6 | const internals = { 7 | text: '{ "a": 5, "b": 6, "proto": { "x": 7 }, "c": { "d": 0, "e": "text", "\\u005f\\u005fproto": { "y": 8 }, "f": { "g": 2 } } }', 8 | suspectRx: /"(?:_|\\u005f)(?:_|\\u005f)(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006f)(?:t|\\u0074)(?:o|\\u006f)(?:_|\\u005f)(?:_|\\u005f)"/ 9 | } 10 | 11 | const suite = new Benchmark.Suite() 12 | 13 | suite 14 | .add('JSON.parse', () => { 15 | JSON.parse(internals.text) 16 | }) 17 | .add('secure-json-parse parse', () => { 18 | sjson.parse(internals.text) 19 | }) 20 | .add('secure-json-parse safeParse', () => { 21 | sjson.safeParse(internals.text) 22 | }) 23 | .add('reviver', () => { 24 | JSON.parse(internals.text, internals.reviver) 25 | }) 26 | .on('cycle', (event) => { 27 | console.log(String(event.target)) 28 | }) 29 | .on('complete', function () { 30 | console.log('Fastest is ' + this.filter('fastest').map('name')) 31 | }) 32 | .run({ async: true }) 33 | 34 | internals.reviver = function (key, value) { 35 | if (key.match(internals.suspectRx)) { 36 | return undefined 37 | } 38 | 39 | return value 40 | } 41 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "valid": "node valid.js", 6 | "ignore": "node ignore.js", 7 | "no_proto": "node no__proto__.js", 8 | "remove": "node remove.js", 9 | "throw": "node throw.js", 10 | "all": "node --version && npm run valid && npm run ignore && npm run no_proto && npm run remove && npm run throw" 11 | }, 12 | "dependencies": { 13 | "benchmark": "^2.1.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benchmarks/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const sjson = require('..') 5 | 6 | const internals = { 7 | text: '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 8 | } 9 | 10 | const suite = new Benchmark.Suite() 11 | 12 | suite 13 | .add('JSON.parse', () => { 14 | JSON.parse(internals.text) 15 | }) 16 | .add('secure-json-parse parse', () => { 17 | sjson.parse(internals.text, { protoAction: 'remove' }) 18 | }) 19 | .add('secure-json-parse safeParse', () => { 20 | sjson.safeParse(internals.text) 21 | }) 22 | .add('reviver', () => { 23 | JSON.parse(internals.text, internals.reviver) 24 | }) 25 | .on('cycle', (event) => { 26 | console.log(String(event.target)) 27 | }) 28 | .on('complete', function () { 29 | console.log('Fastest is ' + this.filter('fastest').map('name')) 30 | }) 31 | .run({ async: true }) 32 | 33 | internals.reviver = function (key, value) { 34 | if (key === '__proto__') { 35 | return undefined 36 | } 37 | 38 | return value 39 | } 40 | -------------------------------------------------------------------------------- /benchmarks/throw.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const sjson = require('..') 5 | 6 | const internals = { 7 | text: '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }', 8 | invalid: '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } } }' 9 | } 10 | 11 | const suite = new Benchmark.Suite() 12 | 13 | suite 14 | .add('JSON.parse valid', () => { 15 | JSON.parse(internals.text) 16 | }) 17 | .add('JSON.parse error', () => { 18 | try { 19 | JSON.parse(internals.invalid) 20 | } catch { } 21 | }) 22 | .add('secure-json-parse parse', () => { 23 | try { 24 | sjson.parse(internals.invalid) 25 | } catch { } 26 | }) 27 | .add('secure-json-parse safeParse', () => { 28 | sjson.safeParse(internals.invalid) 29 | }) 30 | .add('reviver', () => { 31 | try { 32 | JSON.parse(internals.invalid, internals.reviver) 33 | } catch { } 34 | }) 35 | .on('cycle', (event) => { 36 | console.log(String(event.target)) 37 | }) 38 | .on('complete', function () { 39 | console.log('Fastest is ' + this.filter('fastest').map('name')) 40 | }) 41 | .run({ async: true }) 42 | 43 | internals.reviver = function (key, value) { 44 | if (key === '__proto__') { 45 | throw new Error('kaboom') 46 | } 47 | 48 | return value 49 | } 50 | -------------------------------------------------------------------------------- /benchmarks/valid.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const sjson = require('..') 5 | 6 | const internals = { 7 | text: '{ "a": 5, "b": 6, "c": { "d": 0, "e": "text", "f": { "g": 2 } } }', 8 | proto: '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 9 | } 10 | 11 | const suite = new Benchmark.Suite() 12 | 13 | suite 14 | .add('JSON.parse', () => { 15 | JSON.parse(internals.text) 16 | }) 17 | .add('JSON.parse proto', () => { 18 | JSON.parse(internals.proto) 19 | }) 20 | .add('secure-json-parse parse', () => { 21 | sjson.parse(internals.text) 22 | }) 23 | .add('secure-json-parse parse proto', () => { 24 | sjson.parse(internals.text, { constructorAction: 'ignore', protoAction: 'ignore' }) 25 | }) 26 | .add('secure-json-parse safeParse', () => { 27 | sjson.safeParse(internals.text) 28 | }) 29 | .add('secure-json-parse safeParse proto', () => { 30 | sjson.safeParse(internals.proto) 31 | }) 32 | .add('JSON.parse reviver', () => { 33 | JSON.parse(internals.text, internals.reviver) 34 | }) 35 | .on('cycle', (event) => { 36 | console.log(String(event.target)) 37 | }) 38 | .on('complete', function () { 39 | console.log('Fastest is ' + this.filter('fastest').map('name')) 40 | }) 41 | .run({ async: true }) 42 | 43 | internals.reviver = function (key, value) { 44 | if (key === '__proto__') { 45 | return undefined 46 | } 47 | 48 | return value 49 | } 50 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasBuffer = typeof Buffer !== 'undefined' 4 | const suspectProtoRx = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/ 5 | const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/ 6 | 7 | function _parse (text, reviver, options) { 8 | // Normalize arguments 9 | if (options == null) { 10 | if (reviver !== null && typeof reviver === 'object') { 11 | options = reviver 12 | reviver = undefined 13 | } 14 | } 15 | 16 | if (hasBuffer && Buffer.isBuffer(text)) { 17 | text = text.toString() 18 | } 19 | 20 | // BOM checker 21 | if (text && text.charCodeAt(0) === 0xFEFF) { 22 | text = text.slice(1) 23 | } 24 | 25 | // Parse normally, allowing exceptions 26 | const obj = JSON.parse(text, reviver) 27 | 28 | // Ignore null and non-objects 29 | if (obj === null || typeof obj !== 'object') { 30 | return obj 31 | } 32 | 33 | const protoAction = (options && options.protoAction) || 'error' 34 | const constructorAction = (options && options.constructorAction) || 'error' 35 | 36 | // options: 'error' (default) / 'remove' / 'ignore' 37 | if (protoAction === 'ignore' && constructorAction === 'ignore') { 38 | return obj 39 | } 40 | 41 | if (protoAction !== 'ignore' && constructorAction !== 'ignore') { 42 | if (suspectProtoRx.test(text) === false && suspectConstructorRx.test(text) === false) { 43 | return obj 44 | } 45 | } else if (protoAction !== 'ignore' && constructorAction === 'ignore') { 46 | if (suspectProtoRx.test(text) === false) { 47 | return obj 48 | } 49 | } else { 50 | if (suspectConstructorRx.test(text) === false) { 51 | return obj 52 | } 53 | } 54 | 55 | // Scan result for proto keys 56 | return filter(obj, { protoAction, constructorAction, safe: options && options.safe }) 57 | } 58 | 59 | function filter (obj, { protoAction = 'error', constructorAction = 'error', safe } = {}) { 60 | let next = [obj] 61 | 62 | while (next.length) { 63 | const nodes = next 64 | next = [] 65 | 66 | for (const node of nodes) { 67 | if (protoAction !== 'ignore' && Object.prototype.hasOwnProperty.call(node, '__proto__')) { // Avoid calling node.hasOwnProperty directly 68 | if (safe === true) { 69 | return null 70 | } else if (protoAction === 'error') { 71 | throw new SyntaxError('Object contains forbidden prototype property') 72 | } 73 | 74 | delete node.__proto__ // eslint-disable-line no-proto 75 | } 76 | 77 | if (constructorAction !== 'ignore' && 78 | Object.prototype.hasOwnProperty.call(node, 'constructor') && 79 | Object.prototype.hasOwnProperty.call(node.constructor, 'prototype')) { // Avoid calling node.hasOwnProperty directly 80 | if (safe === true) { 81 | return null 82 | } else if (constructorAction === 'error') { 83 | throw new SyntaxError('Object contains forbidden prototype property') 84 | } 85 | 86 | delete node.constructor 87 | } 88 | 89 | for (const key in node) { 90 | const value = node[key] 91 | if (value && typeof value === 'object') { 92 | next.push(value) 93 | } 94 | } 95 | } 96 | } 97 | return obj 98 | } 99 | 100 | function parse (text, reviver, options) { 101 | const { stackTraceLimit } = Error 102 | Error.stackTraceLimit = 0 103 | try { 104 | return _parse(text, reviver, options) 105 | } finally { 106 | Error.stackTraceLimit = stackTraceLimit 107 | } 108 | } 109 | 110 | function safeParse (text, reviver) { 111 | const { stackTraceLimit } = Error 112 | Error.stackTraceLimit = 0 113 | try { 114 | return _parse(text, reviver, { safe: true }) 115 | } catch { 116 | return undefined 117 | } finally { 118 | Error.stackTraceLimit = stackTraceLimit 119 | } 120 | } 121 | 122 | module.exports = parse 123 | module.exports.default = parse 124 | module.exports.parse = parse 125 | module.exports.safeParse = safeParse 126 | module.exports.scan = filter 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-json-parse", 3 | "version": "4.0.0", 4 | "description": "JSON parse with prototype poisoning protection", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "benchmark": "cd benchmarks && npm install && npm run all", 10 | "lint": "eslint", 11 | "lint:fix": "eslint --fix", 12 | "test": "nyc npm run test:unit && npm run test:typescript", 13 | "test:unit": "tape \"test/*.test.js\"", 14 | "test:typescript": "tsd", 15 | "test:browser": "airtap test/*.test.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/fastify/secure-json-parse.git" 20 | }, 21 | "author": "Eran Hammer ", 22 | "contributors": [ 23 | { 24 | "name": "Matteo Collina", 25 | "email": "hello@matteocollina.com" 26 | }, 27 | { 28 | "name": "Tomas Della Vedova", 29 | "url": "http://delved.org" 30 | }, 31 | { 32 | "name": "Aras Abbasi", 33 | "email": "aras.abbasi@gmail.com" 34 | }, 35 | { 36 | "name": "Frazer Smith", 37 | "email": "frazer.dev@icloud.com", 38 | "url": "https://github.com/fdawgs" 39 | } 40 | ], 41 | "keywords": [ 42 | "JSON", 43 | "parse", 44 | "safe", 45 | "security", 46 | "prototype", 47 | "pollution" 48 | ], 49 | "license": "BSD-3-Clause", 50 | "bugs": { 51 | "url": "https://github.com/fastify/secure-json-parse/issues" 52 | }, 53 | "homepage": "https://github.com/fastify/secure-json-parse#readme", 54 | "funding": [ 55 | { 56 | "type": "github", 57 | "url": "https://github.com/sponsors/fastify" 58 | }, 59 | { 60 | "type": "opencollective", 61 | "url": "https://opencollective.com/fastify" 62 | } 63 | ], 64 | "devDependencies": { 65 | "@fastify/pre-commit": "^2.1.0", 66 | "airtap": "^5.0.0", 67 | "airtap-playwright": "^1.0.1", 68 | "eslint": "^9.17.0", 69 | "neostandard": "^0.12.0", 70 | "nyc": "^17.0.0", 71 | "playwright": "^1.43.1", 72 | "tape": "^5.7.5", 73 | "tsd": "^0.32.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tape') 4 | const j = require('..') 5 | 6 | test('parse', t => { 7 | t.test('parses object string', t => { 8 | t.deepEqual( 9 | j.parse('{"a": 5, "b": 6}'), 10 | JSON.parse('{"a": 5, "b": 6}') 11 | ) 12 | t.end() 13 | }) 14 | 15 | t.test('parses null string', t => { 16 | t.strictEqual( 17 | j.parse('null'), 18 | JSON.parse('null') 19 | ) 20 | t.end() 21 | }) 22 | 23 | t.test('parses 0 string', t => { 24 | t.strictEqual( 25 | j.parse('0'), 26 | JSON.parse('0') 27 | ) 28 | t.end() 29 | }) 30 | 31 | t.test('parses string string', t => { 32 | t.strictEqual( 33 | j.parse('"X"'), 34 | JSON.parse('"X"') 35 | ) 36 | t.end() 37 | }) 38 | 39 | t.test('parses buffer', t => { 40 | t.strictEqual( 41 | j.parse(Buffer.from('"X"')), 42 | JSON.parse(Buffer.from('"X"')) 43 | ) 44 | t.end() 45 | }) 46 | 47 | t.test('parses object string (reviver)', t => { 48 | const reviver = (_key, value) => { 49 | return typeof value === 'number' ? value + 1 : value 50 | } 51 | 52 | t.deepEqual( 53 | j.parse('{"a": 5, "b": 6}', reviver), 54 | JSON.parse('{"a": 5, "b": 6}', reviver) 55 | ) 56 | t.end() 57 | }) 58 | 59 | t.test('protoAction', t => { 60 | t.test('sanitizes object string (reviver, options)', t => { 61 | const reviver = (_key, value) => { 62 | return typeof value === 'number' ? value + 1 : value 63 | } 64 | 65 | t.deepEqual( 66 | j.parse('{"a": 5, "b": 6,"__proto__": { "x": 7 }}', reviver, { protoAction: 'remove' }), 67 | { a: 6, b: 7 } 68 | ) 69 | t.end() 70 | }) 71 | 72 | t.test('sanitizes object string (options)', t => { 73 | t.deepEqual( 74 | j.parse('{"a": 5, "b": 6,"__proto__": { "x": 7 }}', { protoAction: 'remove' }), 75 | { a: 5, b: 6 } 76 | ) 77 | t.end() 78 | }) 79 | 80 | t.test('sanitizes object string (null, options)', t => { 81 | t.deepEqual( 82 | j.parse('{"a": 5, "b": 6,"__proto__": { "x": 7 }}', null, { protoAction: 'remove' }), 83 | { a: 5, b: 6 } 84 | ) 85 | t.end() 86 | }) 87 | 88 | t.test('sanitizes object string (null, options)', t => { 89 | t.deepEqual( 90 | j.parse('{"a": 5, "b": 6,"__proto__": { "x": 7 }}', { protoAction: 'remove' }), 91 | { a: 5, b: 6 } 92 | ) 93 | t.end() 94 | }) 95 | 96 | t.test('sanitizes nested object string', t => { 97 | t.deepEqual( 98 | j.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }', { protoAction: 'remove' }), 99 | { a: 5, b: 6, c: { d: 0, e: 'text', f: { g: 2 } } } 100 | ) 101 | t.end() 102 | }) 103 | 104 | t.test('ignores proto property', t => { 105 | t.deepEqual( 106 | j.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }', { protoAction: 'ignore' }), 107 | JSON.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }') 108 | ) 109 | t.end() 110 | }) 111 | 112 | t.test('ignores proto value', t => { 113 | t.deepEqual( 114 | j.parse('{"a": 5, "b": "__proto__"}'), 115 | { a: 5, b: '__proto__' } 116 | ) 117 | t.end() 118 | }) 119 | 120 | t.test('errors on proto property', t => { 121 | t.throws(() => j.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }'), SyntaxError) 122 | t.throws(() => j.parse('{ "a": 5, "b": 6, "__proto__" : { "x": 7 } }'), SyntaxError) 123 | t.throws(() => j.parse('{ "a": 5, "b": 6, "__proto__" \n\r\t : { "x": 7 } }'), SyntaxError) 124 | t.throws(() => j.parse('{ "a": 5, "b": 6, "__proto__" \n \r \t : { "x": 7 } }'), SyntaxError) 125 | t.end() 126 | }) 127 | 128 | t.test('errors on proto property (null, null)', t => { 129 | t.throws(() => j.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }', null, null), SyntaxError) 130 | t.end() 131 | }) 132 | 133 | t.test('errors on proto property (explicit options)', t => { 134 | t.throws(() => j.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }', { protoAction: 'error' }), SyntaxError) 135 | t.end() 136 | }) 137 | 138 | t.test('errors on proto property (unicode)', t => { 139 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u005f_proto__": { "x": 7 } }'), SyntaxError) 140 | t.throws(() => j.parse('{ "a": 5, "b": 6, "_\\u005fp\\u0072oto__": { "x": 7 } }'), SyntaxError) 141 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u005f\\u005f\\u0070\\u0072\\u006f\\u0074\\u006f\\u005f\\u005f": { "x": 7 } }'), SyntaxError) 142 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u005F_proto__": { "x": 7 } }'), SyntaxError) 143 | t.throws(() => j.parse('{ "a": 5, "b": 6, "_\\u005Fp\\u0072oto__": { "x": 7 } }'), SyntaxError) 144 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u005F\\u005F\\u0070\\u0072\\u006F\\u0074\\u006F\\u005F\\u005F": { "x": 7 } }'), SyntaxError) 145 | t.end() 146 | }) 147 | 148 | t.test('should reset stackTraceLimit', t => { 149 | const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 150 | Error.stackTraceLimit = 42 151 | t.throws(() => j.parse(text)) 152 | t.same(Error.stackTraceLimit, 42) 153 | t.end() 154 | }) 155 | 156 | t.end() 157 | }) 158 | 159 | t.test('constructorAction', t => { 160 | t.test('sanitizes object string (reviver, options)', t => { 161 | const reviver = (_key, value) => { 162 | return typeof value === 'number' ? value + 1 : value 163 | } 164 | 165 | t.deepEqual( 166 | j.parse('{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }', reviver, { constructorAction: 'remove' }), 167 | { a: 6, b: 7 } 168 | ) 169 | t.end() 170 | }) 171 | 172 | t.test('sanitizes object string (options)', t => { 173 | t.deepEqual( 174 | j.parse('{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }', { constructorAction: 'remove' }), 175 | { a: 5, b: 6 } 176 | ) 177 | t.end() 178 | }) 179 | 180 | t.test('sanitizes object string (null, options)', t => { 181 | t.deepEqual( 182 | j.parse('{"a": 5, "b": 6,"constructor":{"prototype":{"bar":"baz"}} }', null, { constructorAction: 'remove' }), 183 | { a: 5, b: 6 } 184 | ) 185 | t.end() 186 | }) 187 | 188 | t.test('sanitizes object string (null, options)', t => { 189 | t.deepEqual( 190 | j.parse('{"a": 5, "b": 6,"constructor":{"prototype":{"bar":"baz"}} }', { constructorAction: 'remove' }), 191 | { a: 5, b: 6 } 192 | ) 193 | t.end() 194 | }) 195 | 196 | t.test('sanitizes object string (no prototype key)', t => { 197 | t.deepEqual( 198 | j.parse('{"a": 5, "b": 6,"constructor":{"bar":"baz"} }', { constructorAction: 'remove' }), 199 | { a: 5, b: 6, constructor: { bar: 'baz' } } 200 | ) 201 | t.end() 202 | }) 203 | 204 | t.test('sanitizes nested object string', t => { 205 | t.deepEqual( 206 | j.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "c": { "d": 0, "e": "text", "constructor":{"prototype":{"bar":"baz"}}, "f": { "g": 2 } } }', { constructorAction: 'remove' }), 207 | { a: 5, b: 6, c: { d: 0, e: 'text', f: { g: 2 } } } 208 | ) 209 | t.end() 210 | }) 211 | 212 | t.test('ignores proto property', t => { 213 | t.deepEqual( 214 | j.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }', { constructorAction: 'ignore' }), 215 | JSON.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }') 216 | ) 217 | t.end() 218 | }) 219 | 220 | t.test('ignores proto value', t => { 221 | t.deepEqual( 222 | j.parse('{"a": 5, "b": "constructor"}'), 223 | { a: 5, b: 'constructor' } 224 | ) 225 | t.end() 226 | }) 227 | 228 | t.test('errors on proto property', t => { 229 | t.throws(() => j.parse('{ "a": 5, "b": 6, "constructor": {"prototype":{"bar":"baz"}} }'), SyntaxError) 230 | t.throws(() => j.parse('{ "a": 5, "b": 6, "constructor" : {"prototype":{"bar":"baz"}} }'), SyntaxError) 231 | t.throws(() => j.parse('{ "a": 5, "b": 6, "constructor" \n\r\t : {"prototype":{"bar":"baz"}} }'), SyntaxError) 232 | t.throws(() => j.parse('{ "a": 5, "b": 6, "constructor" \n \r \t : {"prototype":{"bar":"baz"}} }'), SyntaxError) 233 | t.end() 234 | }) 235 | 236 | t.test('Should not throw if the constructor key hasn\'t a child named prototype', t => { 237 | t.doesNotThrow(() => j.parse('{ "a": 5, "b": 6, "constructor":{"bar":"baz"} }', null, null), SyntaxError) 238 | t.end() 239 | }) 240 | 241 | t.test('errors on proto property (null, null)', t => { 242 | t.throws(() => j.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }', null, null), SyntaxError) 243 | t.end() 244 | }) 245 | 246 | t.test('errors on proto property (explicit options)', t => { 247 | t.throws(() => j.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }', { constructorAction: 'error' }), SyntaxError) 248 | t.end() 249 | }) 250 | 251 | t.test('errors on proto property (unicode)', t => { 252 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u0063\\u006fnstructor": {"prototype":{"bar":"baz"}} }'), SyntaxError) 253 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u0063\\u006f\\u006e\\u0073\\u0074ructor": {"prototype":{"bar":"baz"}} }'), SyntaxError) 254 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u0063\\u006f\\u006e\\u0073\\u0074\\u0072\\u0075\\u0063\\u0074\\u006f\\u0072": {"prototype":{"bar":"baz"}} }'), SyntaxError) 255 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u0063\\u006Fnstructor": {"prototype":{"bar":"baz"}} }'), SyntaxError) 256 | t.throws(() => j.parse('{ "a": 5, "b": 6, "\\u0063\\u006F\\u006E\\u0073\\u0074\\u0072\\u0075\\u0063\\u0074\\u006F\\u0072": {"prototype":{"bar":"baz"}} }'), SyntaxError) 257 | t.end() 258 | }) 259 | 260 | t.end() 261 | }) 262 | 263 | t.test('protoAction and constructorAction', t => { 264 | t.test('protoAction=remove constructorAction=remove', t => { 265 | t.deepEqual( 266 | j.parse( 267 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 268 | { protoAction: 'remove', constructorAction: 'remove' } 269 | ), 270 | { a: 5, b: 6 } 271 | ) 272 | t.end() 273 | }) 274 | 275 | t.test('protoAction=ignore constructorAction=remove', t => { 276 | t.deepEqual( 277 | j.parse( 278 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 279 | { protoAction: 'ignore', constructorAction: 'remove' } 280 | ), 281 | JSON.parse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }') 282 | ) 283 | t.end() 284 | }) 285 | 286 | t.test('protoAction=remove constructorAction=ignore', t => { 287 | t.deepEqual( 288 | j.parse( 289 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 290 | { protoAction: 'remove', constructorAction: 'ignore' } 291 | ), 292 | JSON.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }') 293 | ) 294 | t.end() 295 | }) 296 | 297 | t.test('protoAction=ignore constructorAction=ignore', t => { 298 | t.deepEqual( 299 | j.parse( 300 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 301 | { protoAction: 'ignore', constructorAction: 'ignore' } 302 | ), 303 | JSON.parse('{ "a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }') 304 | ) 305 | t.end() 306 | }) 307 | 308 | t.test('protoAction=error constructorAction=ignore', t => { 309 | t.throws(() => j.parse( 310 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 311 | { protoAction: 'error', constructorAction: 'ignore' } 312 | ), SyntaxError) 313 | t.end() 314 | }) 315 | 316 | t.test('protoAction=ignore constructorAction=error', t => { 317 | t.throws(() => j.parse( 318 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 319 | { protoAction: 'ignore', constructorAction: 'error' } 320 | ), SyntaxError) 321 | t.end() 322 | }) 323 | 324 | t.test('protoAction=error constructorAction=error', t => { 325 | t.throws(() => j.parse( 326 | '{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}}, "__proto__": { "x": 7 } }', 327 | { protoAction: 'error', constructorAction: 'error' } 328 | ), SyntaxError) 329 | t.end() 330 | }) 331 | 332 | t.end() 333 | }) 334 | 335 | t.test('sanitizes nested object string', t => { 336 | const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 337 | 338 | const obj = j.parse(text, { protoAction: 'remove' }) 339 | t.deepEqual(obj, { a: 5, b: 6, c: { d: 0, e: 'text', f: { g: 2 } } }) 340 | t.end() 341 | }) 342 | 343 | t.test('errors on constructor property', t => { 344 | const text = '{ "a": 5, "b": 6, "constructor": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 345 | 346 | t.throws(() => j.parse(text), SyntaxError) 347 | t.end() 348 | }) 349 | 350 | t.test('errors on proto property', t => { 351 | const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 352 | 353 | t.throws(() => j.parse(text), SyntaxError) 354 | t.end() 355 | }) 356 | 357 | t.test('errors on constructor property', t => { 358 | const text = '{ "a": 5, "b": 6, "constructor": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 359 | 360 | t.throws(() => j.parse(text), SyntaxError) 361 | t.end() 362 | }) 363 | 364 | t.test('does not break when hasOwnProperty is overwritten', t => { 365 | const text = '{ "a": 5, "b": 6, "hasOwnProperty": "text", "__proto__": { "x": 7 } }' 366 | 367 | const obj = j.parse(text, { protoAction: 'remove' }) 368 | t.deepEqual(obj, { a: 5, b: 6, hasOwnProperty: 'text' }) 369 | t.end() 370 | }) 371 | t.end() 372 | }) 373 | 374 | test('safeParse', t => { 375 | t.test('parses buffer', t => { 376 | t.strictEqual( 377 | j.safeParse(Buffer.from('"X"')), 378 | JSON.parse(Buffer.from('"X"')) 379 | ) 380 | t.end() 381 | }) 382 | 383 | t.test('should reset stackTraceLimit', t => { 384 | const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 385 | Error.stackTraceLimit = 42 386 | t.same(j.safeParse(text), null) 387 | t.same(Error.stackTraceLimit, 42) 388 | t.end() 389 | }) 390 | 391 | t.test('sanitizes nested object string', t => { 392 | const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 393 | 394 | t.same(j.safeParse(text), null) 395 | t.end() 396 | }) 397 | 398 | t.test('returns null on constructor property', t => { 399 | const text = '{ "a": 5, "b": 6, "constructor": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 400 | 401 | t.same(j.safeParse(text), null) 402 | t.end() 403 | }) 404 | 405 | t.test('returns null on proto property', t => { 406 | const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 407 | 408 | t.same(j.safeParse(text), null) 409 | t.end() 410 | }) 411 | 412 | t.test('returns null on constructor property', t => { 413 | const text = '{ "a": 5, "b": 6, "constructor": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }' 414 | 415 | t.same(j.safeParse(text), null) 416 | t.end() 417 | }) 418 | 419 | t.test('parses object string', t => { 420 | t.deepEqual( 421 | j.safeParse('{"a": 5, "b": 6}'), 422 | { a: 5, b: 6 } 423 | ) 424 | t.end() 425 | }) 426 | 427 | t.test('returns null on proto object string', t => { 428 | t.strictEqual( 429 | j.safeParse('{ "a": 5, "b": 6, "__proto__": { "x": 7 } }'), 430 | null 431 | ) 432 | t.end() 433 | }) 434 | 435 | t.test('returns undefined on invalid object string', t => { 436 | t.strictEqual( 437 | j.safeParse('{"a": 5, "b": 6'), 438 | undefined 439 | ) 440 | t.end() 441 | }) 442 | 443 | t.test('sanitizes object string (options)', t => { 444 | t.deepEqual( 445 | j.safeParse('{"a": 5, "b": 6, "constructor":{"prototype":{"bar":"baz"}} }'), 446 | null 447 | ) 448 | t.end() 449 | }) 450 | 451 | t.test('sanitizes object string (no prototype key)', t => { 452 | t.deepEqual( 453 | j.safeParse('{"a": 5, "b": 6,"constructor":{"bar":"baz"} }'), 454 | { a: 5, b: 6, constructor: { bar: 'baz' } } 455 | ) 456 | t.end() 457 | }) 458 | 459 | t.end() 460 | }) 461 | 462 | test('parse string with BOM', t => { 463 | const theJson = { hello: 'world' } 464 | const buffer = Buffer.concat([ 465 | Buffer.from([239, 187, 191]), // the utf8 BOM 466 | Buffer.from(JSON.stringify(theJson)) 467 | ]) 468 | t.deepEqual(j.parse(buffer.toString()), theJson) 469 | t.end() 470 | }) 471 | 472 | test('parse buffer with BOM', t => { 473 | const theJson = { hello: 'world' } 474 | const buffer = Buffer.concat([ 475 | Buffer.from([239, 187, 191]), // the utf8 BOM 476 | Buffer.from(JSON.stringify(theJson)) 477 | ]) 478 | t.deepEqual(j.parse(buffer), theJson) 479 | t.end() 480 | }) 481 | 482 | test('safeParse string with BOM', t => { 483 | const theJson = { hello: 'world' } 484 | const buffer = Buffer.concat([ 485 | Buffer.from([239, 187, 191]), // the utf8 BOM 486 | Buffer.from(JSON.stringify(theJson)) 487 | ]) 488 | t.deepEqual(j.safeParse(buffer.toString()), theJson) 489 | t.end() 490 | }) 491 | 492 | test('safeParse buffer with BOM', t => { 493 | const theJson = { hello: 'world' } 494 | const buffer = Buffer.concat([ 495 | Buffer.from([239, 187, 191]), // the utf8 BOM 496 | Buffer.from(JSON.stringify(theJson)) 497 | ]) 498 | t.deepEqual(j.safeParse(buffer), theJson) 499 | t.end() 500 | }) 501 | 502 | test('scan handles optional options', t => { 503 | t.doesNotThrow(() => j.scan({ a: 'b' })) 504 | t.end() 505 | }) 506 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | type Parse = typeof parse 2 | 3 | declare namespace parse { 4 | export type ParseOptions = { 5 | /** 6 | * What to do when a `__proto__` key is found. 7 | * - `'error'` - throw a `SyntaxError` when a `__proto__` key is found. This is the default value. 8 | * - `'remove'` - deletes any `__proto__` keys from the result object. 9 | * - `'ignore'` - skips all validation (same as calling `JSON.parse()` directly). 10 | */ 11 | protoAction?: 'error' | 'remove' | 'ignore'; 12 | /** 13 | * What to do when a `constructor` key is found. 14 | * - `'error'` - throw a `SyntaxError` when a `constructor.prototype` key is found. This is the default value. 15 | * - `'remove'` - deletes any `constructor` keys from the result object. 16 | * - `'ignore'` - skips all validation (same as calling `JSON.parse()` directly). 17 | */ 18 | constructorAction?: 'error' | 'remove' | 'ignore'; 19 | } 20 | 21 | export type ScanOptions = ParseOptions 22 | 23 | export type Reviver = (this: any, key: string, value: any) => any 24 | 25 | /** 26 | * Parses a given JSON-formatted text into an object. 27 | * 28 | * @param text The JSON text string. 29 | * @param reviver The `JSON.parse()` optional `reviver` argument. 30 | * @param options Optional configuration object. 31 | * @returns The parsed object. 32 | */ 33 | export const parse: Parse 34 | 35 | /** 36 | * Parses a given JSON-formatted text into an object. 37 | * 38 | * @param text The JSON text string. 39 | * @param reviver The `JSON.parse()` optional `reviver` argument. 40 | * @returns The parsed object, or `undefined` if there was an error or if the JSON contained possibly insecure properties. 41 | */ 42 | export function safeParse (text: string | Buffer, reviver?: Reviver | null): any 43 | 44 | /** 45 | * Scans a given object for prototype properties. 46 | * 47 | * @param obj The object being scanned. 48 | * @param options Optional configuration object. 49 | * @returns The object, or `null` if onError is set to `nullify` 50 | */ 51 | export function scan (obj: { [key: string | number]: any }, options?: ParseOptions): any 52 | 53 | export { parse as default } 54 | } 55 | 56 | declare function parse (text: string | Buffer, options?: parse.ParseOptions): any 57 | declare function parse (text: string | Buffer, reviver?: parse.Reviver | null, options?: parse.ParseOptions): any 58 | export = parse 59 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectError } from 'tsd' 2 | import sjson from '..' 3 | 4 | expectError(sjson.parse(null)) 5 | expectType(sjson.parse('{"anything":0}')) 6 | 7 | sjson.parse('"test"', null, { protoAction: 'remove' }) 8 | expectError(sjson.parse('"test"', null, { protoAction: 'incorrect' })) 9 | sjson.parse('"test"', null, { constructorAction: 'ignore' }) 10 | expectError(sjson.parse('"test"', null, { constructorAction: 'incorrect' })) 11 | expectError(sjson.parse('"test"', { constructorAction: 'incorrect' })) 12 | sjson.parse('test', { constructorAction: 'remove' }) 13 | sjson.parse('test', { protoAction: 'ignore' }) 14 | sjson.parse('test', () => {}, { protoAction: 'ignore', constructorAction: 'remove' }) 15 | 16 | sjson.safeParse('"test"', null) 17 | sjson.safeParse('"test"') 18 | expectError(sjson.safeParse(null)) 19 | 20 | sjson.scan({}, { protoAction: 'remove' }) 21 | sjson.scan({}, { protoAction: 'ignore' }) 22 | sjson.scan({}, { constructorAction: 'error' }) 23 | sjson.scan({}, { constructorAction: 'ignore' }) 24 | sjson.scan([], {}) 25 | 26 | declare const input: Buffer 27 | sjson.parse(input) 28 | sjson.safeParse(input) 29 | 30 | sjson.parse('{"anything":0}', (key, value) => { 31 | expectType(key) 32 | }) 33 | sjson.safeParse('{"anything":0}', (key, value) => { 34 | expectType(key) 35 | }) 36 | --------------------------------------------------------------------------------