Skip to content

Commit cd3e3f7

Browse files
committed
Add tests for security fixes and input validation
1 parent af9642d commit cd3e3f7

File tree

4 files changed

+111
-13
lines changed

4 files changed

+111
-13
lines changed

test/integration.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('Integration tests', () => {
3030
it('should load PackageURLBuilder and work correctly', async () => {
3131
const { pkgPath } = await isolatePackage(packagePath)
3232

33-
const { PackageURLBuilder } = await import(`${pkgPath}/dist/package-url.js`)
33+
const { PackageURLBuilder } = await import(`${pkgPath}/dist/index.js`)
3434
expect(PackageURLBuilder).toBeDefined()
3535
expect(typeof PackageURLBuilder.create).toBe('function')
3636

test/package-url-builder.test.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ SOFTWARE.
2525
*/
2626
import { describe, expect, it } from 'vitest'
2727

28-
import { PackageURL, PackageURLBuilder } from '../src/package-url.js'
28+
import { PackageURLBuilder } from '../src/package-url-builder.js'
29+
import { PackageURL } from '../src/package-url.js'
2930

3031
describe('PackageURLBuilder', () => {
3132
describe('basic construction', () => {

test/package-url.test.mts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,52 @@ describe('PackageURL', () => {
466466
expect(purl1.version).toBe(purl2.version)
467467
})
468468
})
469+
470+
describe('Input validation', () => {
471+
it('should reject JSON strings exceeding maximum size', () => {
472+
const largeJson = JSON.stringify({ name: 'x'.repeat(1024 * 1024) })
473+
expect(() => PackageURL.fromJSON(largeJson)).toThrow(
474+
'JSON string exceeds maximum size limit of 1048576 bytes',
475+
)
476+
})
477+
478+
it('should reject non-object JSON', () => {
479+
expect(() => PackageURL.fromJSON('[]')).toThrow(
480+
'JSON must parse to an object',
481+
)
482+
expect(() => PackageURL.fromJSON('"string"')).toThrow(
483+
'JSON must parse to an object',
484+
)
485+
expect(() => PackageURL.fromJSON('null')).toThrow(
486+
'JSON must parse to an object',
487+
)
488+
})
489+
490+
it('should prevent prototype pollution in fromJSON', () => {
491+
const maliciousJson =
492+
'{"__proto__":{"isAdmin":true},"type":"npm","name":"test"}'
493+
const purl = PackageURL.fromJSON(maliciousJson)
494+
expect(purl.type).toBe('npm')
495+
expect(purl.name).toBe('test')
496+
// Verify prototype pollution didn't occur.
497+
expect(({} as any).isAdmin).toBeUndefined()
498+
})
499+
500+
it('should reject package URLs exceeding maximum length', () => {
501+
const longUrl = 'pkg:npm/' + 'x'.repeat(4090)
502+
expect(() => PackageURL.fromString(longUrl)).toThrow(
503+
'Package URL exceeds maximum length of 4096 characters',
504+
)
505+
})
506+
507+
it('should handle bounded regex patterns without ReDoS', () => {
508+
// These used to be potential ReDoS vectors with unbounded quantifiers.
509+
const longScheme = 'a'.repeat(300) + '://'
510+
const longType = 'a'.repeat(300) + '/'
511+
512+
// Should not hang with bounded patterns.
513+
expect(() => PackageURL.fromString(longScheme)).toThrow()
514+
expect(() => PackageURL.fromString(longType)).toThrow()
515+
})
516+
})
469517
})

test/purl-edge-cases.test.mts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,19 +210,29 @@ describe('Edge cases and additional coverage', () => {
210210
})
211211

212212
it('should handle very long component values', () => {
213-
// Tests no length limits on components (stress test)
214-
const longString = 'a'.repeat(1000)
213+
// Test with maximum allowed lengths for components.
214+
const maxNamespace = 'a'.repeat(512) // Max namespace length.
215+
const maxName = 'b'.repeat(214) // Max name length.
216+
const maxVersion = 'c'.repeat(256) // Max version length.
215217
const purl = new PackageURL(
216218
'type',
217-
longString,
218-
longString,
219-
longString,
219+
maxNamespace,
220+
maxName,
221+
maxVersion,
220222
undefined,
221223
undefined,
222224
)
223-
expect(purl.namespace).toBe(longString)
224-
expect(purl.name).toBe(longString)
225-
expect(purl.version).toBe(longString)
225+
expect(purl.namespace).toBe(maxNamespace)
226+
expect(purl.name).toBe(maxName)
227+
expect(purl.version).toBe(maxVersion)
228+
229+
// Test that exceeding limits throws errors.
230+
expect(
231+
() => new PackageURL('type', 'a'.repeat(513), 'name', undefined),
232+
).toThrow('"namespace" exceeds maximum length of 512 characters')
233+
expect(() => new PackageURL('type', undefined, 'a'.repeat(215))).toThrow(
234+
'"name" exceeds maximum length of 214 characters',
235+
)
226236
})
227237

228238
it('should preserve exact qualifier order in toString', () => {
@@ -2414,9 +2424,7 @@ describe('Edge cases and additional coverage', () => {
24142424
undefined,
24152425
undefined,
24162426
),
2417-
).toThrow(
2418-
'npm "namespace" and "name" components can not collectively be more than 214 characters',
2419-
)
2427+
).toThrow('"name" exceeds maximum length of 214 characters')
24202428

24212429
// Test npm core module name with throws (line 355)
24222430
// Use a builtin that's not a legacy name (legacy names skip the builtin check)
@@ -2855,4 +2863,45 @@ describe('Edge cases and additional coverage', () => {
28552863
expect(result3).toBe(false)
28562864
})
28572865
})
2866+
2867+
describe('Length validation', () => {
2868+
it('should reject names exceeding maximum length', () => {
2869+
const longName = 'x'.repeat(215)
2870+
expect(validateName(longName, { throws: false })).toBe(false)
2871+
expect(() => validateName(longName, { throws: true })).toThrow(
2872+
'"name" exceeds maximum length of 214 characters',
2873+
)
2874+
})
2875+
2876+
it('should accept names at maximum length', () => {
2877+
const maxName = 'x'.repeat(214)
2878+
expect(validateName(maxName, { throws: false })).toBe(true)
2879+
})
2880+
2881+
it('should reject namespaces exceeding maximum length', () => {
2882+
const longNamespace = 'x'.repeat(513)
2883+
expect(validateNamespace(longNamespace, { throws: false })).toBe(false)
2884+
expect(() => validateNamespace(longNamespace, { throws: true })).toThrow(
2885+
'"namespace" exceeds maximum length of 512 characters',
2886+
)
2887+
})
2888+
2889+
it('should accept namespaces at maximum length', () => {
2890+
const maxNamespace = 'x'.repeat(512)
2891+
expect(validateNamespace(maxNamespace, { throws: false })).toBe(true)
2892+
})
2893+
2894+
it('should reject versions exceeding maximum length', () => {
2895+
const longVersion = 'x'.repeat(257)
2896+
expect(validateVersion(longVersion, { throws: false })).toBe(false)
2897+
expect(() => validateVersion(longVersion, { throws: true })).toThrow(
2898+
'"version" exceeds maximum length of 256 characters',
2899+
)
2900+
})
2901+
2902+
it('should accept versions at maximum length', () => {
2903+
const maxVersion = 'x'.repeat(256)
2904+
expect(validateVersion(maxVersion, { throws: false })).toBe(true)
2905+
})
2906+
})
28582907
})

0 commit comments

Comments
 (0)