├── .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 | [](https://github.com/fastify/secure-json-parse/actions/workflows/ci.yml)
4 | [](https://www.npmjs.com/package/secure-json-parse)
5 | [](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 |
--------------------------------------------------------------------------------