Skip to content

Commit fcea2c0

Browse files
committed
refactor: consolidate JSON parsing under @socketsecurity/lib/json
Remove the `./validation/*` subpath — all JSON parsing now lives under `@socketsecurity/lib/json`. The former split meant callers had to choose between two sibling modules with overlapping purpose: - `@socketsecurity/lib/json/parse` (trusted-source `jsonParse` with Buffer/BOM/filepath-aware errors) - `@socketsecurity/lib/validation/json-parser` (untrusted-source `safeJsonParse` with prototype-pollution reviver + size limits + Zod schema hook) Merged by moving `safeJsonParse` into `src/json/parse.ts` as a sibling to `jsonParse`. The schema-validation branch now delegates to `validateSchema` from `@socketsecurity/lib/schema/validate` instead of hand-normalizing Zod error shapes, so the path/message-join logic lives in exactly one place. Changes: - `src/json/parse.ts` — add `safeJsonParse` (alphabetical order after `jsonParse`), including `prototypePollutionReviver` and `DANGEROUS_KEYS`. Imports `validateSchema` + `Schema<T>` from the `schema/*` module. - `src/json/types.ts` — add `SafeJsonParseOptions` (was in `validation/types.ts`). - `test/unit/validation/json-parser.test.mts` renamed to `test/unit/json/safe-parse.test.mts` with updated import path and describe name. All 24 tests still pass unchanged. - Delete `src/validation/json-parser.ts`, `src/validation/types.ts`, and both empty parent dirs. `Schema` + `ParseResult` types in the old `validation/types.ts` are superseded by `schema/types.ts`. - `package.json` + `docs/api-index.md` — regenerated; the `./validation/*` exports are gone. Consumers migrating: change import { safeJsonParse } from '@socketsecurity/lib/validation/json-parser' to import { safeJsonParse } from '@socketsecurity/lib/json/parse' The function signature is identical.
1 parent 7389b8a commit fcea2c0

7 files changed

Lines changed: 168 additions & 259 deletions

File tree

docs/api-index.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Each entry links to the source module and shows the first sentence of its `@file
55

66
> Regenerate with `node scripts/fix/generate-api-index.mts` after adding or removing exports. Do not edit this file by hand.
77
8-
**Jump to:** [Top-level](#top-level) · [argv/](#argv) · [constants/](#constants) · [cover/](#cover) · [dlx/](#dlx) · [effects/](#effects) · [env/](#env) · [json/](#json) · [packages/](#packages) · [paths/](#paths) · [releases/](#releases) · [schema/](#schema) · [stdio/](#stdio) · [themes/](#themes) · [validation/](#validation)
8+
**Jump to:** [Top-level](#top-level) · [argv/](#argv) · [constants/](#constants) · [cover/](#cover) · [dlx/](#dlx) · [effects/](#effects) · [env/](#env) · [json/](#json) · [packages/](#packages) · [paths/](#paths) · [releases/](#releases) · [schema/](#schema) · [stdio/](#stdio) · [themes/](#themes)
99

1010
## Top-level
1111

@@ -222,10 +222,3 @@ Each entry links to the source module and shows the first sentence of its `@file
222222
| [`@socketsecurity/lib/themes/themes`](../src/themes/themes.ts) | Elegant theme definitions for Socket libraries. |
223223
| [`@socketsecurity/lib/themes/types`](../src/themes/types.ts) | Elegant theme type system. |
224224
| [`@socketsecurity/lib/themes/utils`](../src/themes/utils.ts) | Theme utilities — color resolution and composition. |
225-
226-
## validation/
227-
228-
| Subpath | Description |
229-
| -------------------------------------------------------------------------------- | -------------------------------------------------------- |
230-
| [`@socketsecurity/lib/validation/json-parser`](../src/validation/json-parser.ts) | Safe JSON parsing with validation and security controls. |
231-
| [`@socketsecurity/lib/validation/types`](../src/validation/types.ts) | Validation type definitions. |

package.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -667,14 +667,6 @@
667667
"types": "./dist/url.d.ts",
668668
"default": "./dist/url.js"
669669
},
670-
"./validation/json-parser": {
671-
"types": "./dist/validation/json-parser.d.ts",
672-
"default": "./dist/validation/json-parser.js"
673-
},
674-
"./validation/types": {
675-
"types": "./dist/validation/types.d.ts",
676-
"default": "./dist/validation/types.js"
677-
},
678670
"./versions": {
679671
"types": "./dist/versions.d.ts",
680672
"default": "./dist/versions.js"

src/json/parse.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
/**
22
* @fileoverview JSON parsing utilities with Buffer detection and BOM stripping.
3-
* Provides safe JSON parsing with automatic encoding handling.
3+
* Provides safe JSON parsing with automatic encoding handling, plus
4+
* `safeJsonParse` for untrusted input (prototype-pollution protection +
5+
* size limits + optional schema validation).
46
*/
57

8+
import { validateSchema } from '../schema/validate'
69
import { stripBom } from '../strings'
7-
import type { JsonParseOptions, JsonPrimitive, JsonValue } from './types'
10+
import type { Schema } from '../schema/types'
11+
import type {
12+
JsonParseOptions,
13+
JsonPrimitive,
14+
JsonValue,
15+
SafeJsonParseOptions,
16+
} from './types'
817

918
// IMPORTANT: Do not use destructuring here - use direct assignment instead.
1019
// tsgo has a bug that incorrectly transpiles destructured exports, resulting in
@@ -157,3 +166,106 @@ export function jsonParse(
157166
}
158167
return undefined
159168
}
169+
170+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
171+
172+
/**
173+
* JSON.parse reviver that rejects prototype pollution keys at any depth.
174+
*
175+
* @internal
176+
*/
177+
function prototypePollutionReviver(key: string, value: unknown): unknown {
178+
if (DANGEROUS_KEYS.has(key)) {
179+
throw new Error(
180+
'JSON contains potentially malicious prototype pollution keys',
181+
)
182+
}
183+
return value
184+
}
185+
186+
const DEFAULT_MAX_SIZE = 10 * 1024 * 1024
187+
188+
/**
189+
* Safely parse JSON with optional schema validation and security controls.
190+
* Throws on parse failure, validation failure, or security violation.
191+
*
192+
* Recommended for parsing untrusted JSON (user input, network payloads,
193+
* anything beyond a trust boundary). Layers:
194+
* 1. Size cap (default 10 MB) prevents memory exhaustion.
195+
* 2. Prototype-pollution reviver rejects `__proto__` / `constructor` /
196+
* `prototype` keys at any depth (unless `allowPrototype: true`).
197+
* 3. Optional Zod-shaped schema validation via
198+
* `@socketsecurity/lib/schema/validate`.
199+
*
200+
* For trusted-source reads (package.json, local config files), prefer
201+
* `jsonParse()` — it offers Buffer/BOM handling and filepath-aware error
202+
* messages, without the untrusted-input overhead.
203+
*
204+
* @throws {Error} When `jsonString` exceeds `maxSize`.
205+
* @throws {Error} When JSON parsing fails.
206+
* @throws {Error} When prototype-pollution keys are detected (and
207+
* `allowPrototype` is not `true`).
208+
* @throws {Error} When schema validation fails.
209+
*
210+
* @example
211+
* ```ts
212+
* // Basic parsing with type inference.
213+
* const data = safeJsonParse<User>('{"name":"Alice","age":30}')
214+
*
215+
* // With schema validation.
216+
* import { z } from 'zod'
217+
* const userSchema = z.object({ name: z.string(), age: z.number() })
218+
* const user = safeJsonParse('{"name":"Alice","age":30}', userSchema)
219+
*
220+
* // With size limit.
221+
* const data = safeJsonParse(jsonString, undefined, { maxSize: 1024 })
222+
*
223+
* // Allow prototype keys (DANGEROUS — only for trusted sources).
224+
* const data = safeJsonParse('{"__proto__":{}}', undefined, {
225+
* allowPrototype: true,
226+
* })
227+
* ```
228+
*/
229+
/*@__NO_SIDE_EFFECTS__*/
230+
export function safeJsonParse<T = unknown>(
231+
jsonString: string,
232+
schema?: Schema<T> | undefined,
233+
options: SafeJsonParseOptions = {},
234+
): T {
235+
const { allowPrototype = false, maxSize = DEFAULT_MAX_SIZE } = options
236+
237+
// Size check up front.
238+
const byteLength = Buffer.byteLength(jsonString, 'utf8')
239+
if (byteLength > maxSize) {
240+
throw new Error(
241+
`JSON string exceeds maximum size limit${
242+
maxSize !== DEFAULT_MAX_SIZE ? ` of ${maxSize} bytes` : ''
243+
}`,
244+
)
245+
}
246+
247+
// Parse with the prototype-pollution reviver unless the caller opted out.
248+
let parsed: unknown
249+
try {
250+
parsed = allowPrototype
251+
? JSONParse(jsonString)
252+
: JSONParse(jsonString, prototypePollutionReviver)
253+
} catch (error) {
254+
throw new Error(`Failed to parse JSON: ${error}`)
255+
}
256+
257+
// Optional schema validation — route through validateSchema so the
258+
// normalization logic lives in exactly one place.
259+
if (schema) {
260+
const result = validateSchema(schema, parsed)
261+
if (!result.ok) {
262+
const summary = result.errors
263+
.map(e => `${e.path.join('.') || '(root)'}: ${e.message}`)
264+
.join(', ')
265+
throw new Error(`Validation failed: ${summary}`)
266+
}
267+
return result.value
268+
}
269+
270+
return parsed as T
271+
}

src/json/types.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,57 @@ export type JsonValue = JsonPrimitive | JsonObject | JsonArray
7777
*/
7878
export type JsonReviver = (key: string, value: unknown) => unknown
7979

80+
/**
81+
* Options for `safeJsonParse`: security controls for untrusted JSON.
82+
*
83+
* Distinct from `JsonParseOptions` (which is scoped to reviver /
84+
* error-handling for trusted-source fs reads). Use this type when
85+
* parsing user input, network payloads, or anything beyond a trust
86+
* boundary.
87+
*
88+
* @example
89+
* ```ts
90+
* const options: SafeJsonParseOptions = {
91+
* maxSize: 1024 * 1024, // 1MB limit
92+
* allowPrototype: false // Block prototype pollution
93+
* }
94+
* ```
95+
*/
96+
export interface SafeJsonParseOptions {
97+
/**
98+
* Allow dangerous prototype pollution keys (`__proto__`, `constructor`, `prototype`).
99+
* Set to `true` only if you trust the JSON source completely.
100+
*
101+
* @default false
102+
*
103+
* @example
104+
* ```ts
105+
* // Will throw error by default
106+
* safeJsonParse('{"__proto__": {"polluted": true}}')
107+
*
108+
* // Allows the parse (dangerous!)
109+
* safeJsonParse('{"__proto__": {"polluted": true}}', undefined, {
110+
* allowPrototype: true
111+
* })
112+
* ```
113+
*/
114+
allowPrototype?: boolean | undefined
115+
116+
/**
117+
* Maximum allowed size of JSON string in bytes.
118+
* Prevents memory exhaustion from extremely large payloads.
119+
*
120+
* @default 10_485_760 (10 MB)
121+
*
122+
* @example
123+
* ```ts
124+
* // Limit to 1KB
125+
* safeJsonParse(jsonString, undefined, { maxSize: 1024 })
126+
* ```
127+
*/
128+
maxSize?: number | undefined
129+
}
130+
80131
/**
81132
* Options for JSON parsing operations.
82133
*/

src/validation/json-parser.ts

Lines changed: 0 additions & 115 deletions
This file was deleted.

0 commit comments

Comments
 (0)