From 2311f4a043ef10e2e707dce4f1cdae9c36dd8473 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:37:33 +0000 Subject: [PATCH 1/3] Initial plan From 6e96d641e9408a40d179fb82d8da29a4de6019fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:40:44 +0000 Subject: [PATCH 2/3] fix: add URI length cap in validateExternalUri strict mode to prevent catastrophic backtracking Co-authored-by: ormidales <46538211+ormidales@users.noreply.github.com> --- src/core/GltfLoader.ts | 19 ++++++++++++++----- tests/gltf.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/core/GltfLoader.ts b/src/core/GltfLoader.ts index 5ff41c5..7bb0bf9 100644 --- a/src/core/GltfLoader.ts +++ b/src/core/GltfLoader.ts @@ -35,6 +35,8 @@ const GLB_CHUNK_JSON = 0x4E4F534A; const GLB_CHUNK_BIN = 0x004E4942; const UTF8_DECODER = new TextDecoder(); const MAX_JSON_BUFFER_BYTES = 64 * 1024 * 1024; +/** Maximum URI length accepted in strict mode (guards against catastrophic backtracking). */ +const MAX_URI_LENGTH = 2048; /** * Deep-clone arbitrary JSON-like data into "data-only" structures: @@ -388,11 +390,18 @@ function validateExternalUri(uri: string, bufferIndex: number, strict?: boolean) ); } - if (strict && !candidates.every((v) => /^[A-Za-z0-9._\-/]+$/.test(v))) { - throw new Error( - `Buffer ${bufferIndex}: external URI "${uri}" contains characters not permitted in strict mode. ` + - `Only alphanumeric characters, dots, hyphens, underscores, and forward slashes are allowed.`, - ); + if (strict) { + if (candidates.some((v) => v.length > MAX_URI_LENGTH)) { + throw new Error( + `Buffer ${bufferIndex}: URI exceeds maximum allowed length in strict mode.`, + ); + } + if (!candidates.every((v) => /^[A-Za-z0-9._\-/]+$/.test(v))) { + throw new Error( + `Buffer ${bufferIndex}: external URI "${uri}" contains characters not permitted in strict mode. ` + + `Only alphanumeric characters, dots, hyphens, underscores, and forward slashes are allowed.`, + ); + } } } diff --git a/tests/gltf.test.ts b/tests/gltf.test.ts index 54f9477..c3f62d4 100644 --- a/tests/gltf.test.ts +++ b/tests/gltf.test.ts @@ -1149,6 +1149,39 @@ describe('loadGltf', () => { expect(resolveUri).not.toHaveBeenCalled(); }); + it('rejects URI exceeding 2048 characters in strict mode', async () => { + const { json, bin } = triangleAsset(); + // Build a 2049-character URI using only allowed characters so it would pass + // the allowlist regex — only the length cap should trigger the rejection. + const longUri = 'a'.repeat(2045) + '.bin'; + json.buffers = [{ uri: longUri, byteLength: bin.byteLength }]; + const buffer = jsonToBuffer(json); + const resolveUri = vi.fn().mockResolvedValue(bin); + await expect(loadGltf(buffer, { resolveUri, strict: true })).rejects.toThrow(/exceeds maximum allowed length in strict mode/); + expect(resolveUri).not.toHaveBeenCalled(); + }); + + it('allows URI exactly at 2048 characters in strict mode', async () => { + const { json, bin } = triangleAsset(); + const exactUri = 'a'.repeat(2044) + '.bin'; + json.buffers = [{ uri: exactUri, byteLength: bin.byteLength }]; + const buffer = jsonToBuffer(json); + const resolveUri = vi.fn().mockResolvedValue(bin); + await expect(loadGltf(buffer, { resolveUri, strict: true })).resolves.toMatchObject({ meshes: expect.any(Array) }); + expect(resolveUri).toHaveBeenCalledWith(exactUri); + }); + + it('does not apply URI length cap in non-strict mode', async () => { + const { json, bin } = triangleAsset(); + // URI longer than 2048 chars but with only safe relative characters + const longUri = 'a'.repeat(2045) + '.bin'; + json.buffers = [{ uri: longUri, byteLength: bin.byteLength }]; + const buffer = jsonToBuffer(json); + const resolveUri = vi.fn().mockResolvedValue(bin); + await expect(loadGltf(buffer, { resolveUri })).resolves.toMatchObject({ meshes: expect.any(Array) }); + expect(resolveUri).toHaveBeenCalledWith(longUri); + }); + it('rejects URL-encoded path traversal URI "%2e%2e%2fetc%2fpasswd"', async () => { const { json, bin } = triangleAsset(); json.buffers = [{ uri: '%2e%2e%2fetc%2fpasswd', byteLength: bin.byteLength }]; From ed64cb9e3fa66c93d2fdcbfbe90125385fb86650 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:49:25 +0000 Subject: [PATCH 3/3] fix: clarify MAX_URI_LENGTH comment and include len/max in rejection error Co-authored-by: ormidales <46538211+ormidales@users.noreply.github.com> --- src/core/GltfLoader.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/GltfLoader.ts b/src/core/GltfLoader.ts index 7bb0bf9..97438cb 100644 --- a/src/core/GltfLoader.ts +++ b/src/core/GltfLoader.ts @@ -35,7 +35,7 @@ const GLB_CHUNK_JSON = 0x4E4F534A; const GLB_CHUNK_BIN = 0x004E4942; const UTF8_DECODER = new TextDecoder(); const MAX_JSON_BUFFER_BYTES = 64 * 1024 * 1024; -/** Maximum URI length accepted in strict mode (guards against catastrophic backtracking). */ +/** Maximum URI length accepted in strict mode (guards against excessively long URIs that could cause resource exhaustion). */ const MAX_URI_LENGTH = 2048; /** @@ -392,8 +392,9 @@ function validateExternalUri(uri: string, bufferIndex: number, strict?: boolean) if (strict) { if (candidates.some((v) => v.length > MAX_URI_LENGTH)) { + const len = Math.max(...candidates.map((v) => v.length)); throw new Error( - `Buffer ${bufferIndex}: URI exceeds maximum allowed length in strict mode.`, + `Buffer ${bufferIndex}: URI exceeds maximum allowed length in strict mode (len=${len}, max=${MAX_URI_LENGTH}).`, ); } if (!candidates.every((v) => /^[A-Za-z0-9._\-/]+$/.test(v))) {