Skip to content

Commit 4d3b745

Browse files
committed
Fix ReDoS vulnerabilities and add input validation
- Add bounded quantifiers to regex patterns to prevent ReDoS - Add 1MB size limit and object validation to fromJSON - Add 4096 char limit to parseString for package URLs - Prevent prototype pollution in JSON parsing
1 parent 3291da2 commit 4d3b745

File tree

1 file changed

+42
-5
lines changed

1 file changed

+42
-5
lines changed

src/package-url.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ SOFTWARE.
3131
import { decodePurlComponent } from './decode.js'
3232
import { PurlError } from './error.js'
3333
import { isObject, recursiveFreeze } from './objects.js'
34-
import { PackageURLBuilder } from './package-url-builder.js'
3534
import { PurlComponent } from './purl-component.js'
3635
import { PurlQualifierNames } from './purl-qualifier-names.js'
3736
import { PurlType } from './purl-type.js'
@@ -68,10 +67,12 @@ export type PackageURLObject = {
6867
}
6968

7069
// Pattern to match URLs with schemes other than "pkg".
71-
const OTHER_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//
70+
// Limited to 256 chars for scheme to prevent ReDoS.
71+
const OTHER_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]{0,255}:\/\//
7272

7373
// Pattern to match purl-like strings with type/name format.
74-
const PURL_LIKE_PATTERN = /^[a-zA-Z0-9+.-]+\//
74+
// Limited to 256 chars for type to prevent ReDoS.
75+
const PURL_LIKE_PATTERN = /^[a-zA-Z0-9+.-]{1,256}\//
7576

7677
/**
7778
* Package URL parser and constructor implementing the PURL specification.
@@ -255,11 +256,39 @@ class PackageURL {
255256
if (typeof json !== 'string') {
256257
throw new Error('JSON string argument is required.')
257258
}
259+
260+
// Size limit: 1MB to prevent memory exhaustion.
261+
const MAX_JSON_SIZE = 1024 * 1024
262+
if (json.length > MAX_JSON_SIZE) {
263+
throw new Error(
264+
`JSON string exceeds maximum size limit of ${MAX_JSON_SIZE} bytes.`,
265+
)
266+
}
267+
268+
let parsed
258269
try {
259-
return PackageURL.fromObject(JSON.parse(json))
270+
parsed = JSON.parse(json)
260271
} catch (e) {
261272
throw new Error('Invalid JSON string.', { cause: e })
262273
}
274+
275+
// Validate parsed result is an object.
276+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
277+
throw new Error('JSON must parse to an object.')
278+
}
279+
280+
// Create a safe object without prototype chain to prevent prototype pollution.
281+
const safeObject: PackageURLObject = {
282+
__proto__: null,
283+
type: parsed.type,
284+
namespace: parsed.namespace,
285+
name: parsed.name,
286+
version: parsed.version,
287+
qualifiers: parsed.qualifiers,
288+
subpath: parsed.subpath,
289+
} as PackageURLObject
290+
291+
return PackageURL.fromObject(safeObject)
263292
}
264293

265294
/**
@@ -302,6 +331,15 @@ class PackageURL {
302331
return [undefined, undefined, undefined, undefined, undefined, undefined]
303332
}
304333

334+
// Input length validation to prevent DoS.
335+
// Reasonable limit for a package URL.
336+
const MAX_PURL_LENGTH = 4096
337+
if (purlStr.length > MAX_PURL_LENGTH) {
338+
throw new Error(
339+
`Package URL exceeds maximum length of ${MAX_PURL_LENGTH} characters.`,
340+
)
341+
}
342+
305343
// If the string doesn't start with "pkg:" but looks like a purl format, prepend "pkg:" and try parsing.
306344
if (!purlStr.startsWith('pkg:')) {
307345
// Only auto-prepend "pkg:" if the string looks like a purl (contains a type/name pattern)
@@ -484,7 +522,6 @@ export {
484522
Err,
485523
Ok,
486524
PackageURL,
487-
PackageURLBuilder,
488525
PurlComponent,
489526
PurlQualifierNames,
490527
PurlType,

0 commit comments

Comments
 (0)