From e9779ed3d2986f8ca5f80fe945bfca7174fcdedf Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Mon, 27 Jul 2026 23:26:13 +0100 Subject: [PATCH 1/2] fix: prevent public key validation ReDoS --- .changeset/calm-keys-verify.md | 5 +++++ .../__tests__/ScriptManager.test.ts | 16 ++++++++++++++++ .../modules/ScriptManager/normalizePublicKey.ts | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-keys-verify.md diff --git a/.changeset/calm-keys-verify.md b/.changeset/calm-keys-verify.md new file mode 100644 index 000000000..4fbd4b465 --- /dev/null +++ b/.changeset/calm-keys-verify.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Prevent excessive regular-expression backtracking when validating per-script public keys. diff --git a/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts b/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts index 723ef1064..b92cc0f46 100644 --- a/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts +++ b/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts @@ -401,6 +401,22 @@ describe('ScriptManagerAPI', () => { ); }); + it('should reject a large malformed public key without excessive backtracking', async () => { + ScriptManager.shared.addResolver(async (scriptId) => { + return { + url: Script.getRemoteURL(`http://domain.ext/${scriptId}`), + verifyScriptSignature: 'strict', + publicKey: `-----BEGIN PUBLIC KEY-----${' '.repeat(4096)}x`, + }; + }); + + await expect( + ScriptManager.shared.resolveScript('src_App_js', 'main') + ).rejects.toThrow( + 'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.' + ); + }); + it('should allow public key override with surrounding whitespace', async () => { ScriptManager.shared.addResolver(async (scriptId) => { return { diff --git a/packages/repack/src/modules/ScriptManager/normalizePublicKey.ts b/packages/repack/src/modules/ScriptManager/normalizePublicKey.ts index 208aa0509..fd5f53cbb 100644 --- a/packages/repack/src/modules/ScriptManager/normalizePublicKey.ts +++ b/packages/repack/src/modules/ScriptManager/normalizePublicKey.ts @@ -1,7 +1,7 @@ import { NormalizedScriptLocatorSignatureVerificationMode } from './NativeScriptManager.js'; const PUBLIC_KEY_PEM_PATTERN = - /^-----BEGIN PUBLIC KEY-----\s*[\s\S]+?\s*-----END PUBLIC KEY-----$/; + /^-----BEGIN PUBLIC KEY-----[\s\S]+-----END PUBLIC KEY-----$/; export const INVALID_PUBLIC_KEY_ERROR = 'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.'; From 8bfbf895a0b8fc3a96e9256f0a4174f6cb615a64 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Mon, 27 Jul 2026 23:35:34 +0100 Subject: [PATCH 2/2] test: cover realistic public key formats --- .../__tests__/ScriptManager.test.ts | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts b/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts index b92cc0f46..3172a008e 100644 --- a/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts +++ b/packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts @@ -1,3 +1,6 @@ +import { createPublicKey } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; import NativeScriptManager, { type NormalizedScriptLocator, } from '../NativeScriptManager.js'; @@ -31,6 +34,21 @@ webpackRequire.repack = { globalThis.__webpack_require__ = webpackRequire; +const RSA_PUBLIC_KEY = fs + .readFileSync( + path.join( + __dirname, + '../../../plugins/__tests__/__fixtures__/testRS256.pem.pub' + ), + 'utf8' + ) + .trim(); + +const PKCS1_RSA_PUBLIC_KEY = createPublicKey(RSA_PUBLIC_KEY) + .export({ format: 'pem', type: 'pkcs1' }) + .toString() + .trim(); + class FakeCache { data: Record = {}; @@ -362,8 +380,7 @@ describe('ScriptManagerAPI', () => { return { url: Script.getRemoteURL(`http://domain.ext/${scriptId}`), verifyScriptSignature: 'strict', - publicKey: - '-----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY-----', + publicKey: RSA_PUBLIC_KEY, }; }); @@ -379,8 +396,7 @@ describe('ScriptManagerAPI', () => { method: 'GET', timeout: Script.DEFAULT_TIMEOUT, verifyScriptSignature: 'strict', - publicKey: - '-----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY-----', + publicKey: RSA_PUBLIC_KEY, uniqueId: 'main_src_App_js', }); }); @@ -401,6 +417,38 @@ describe('ScriptManagerAPI', () => { ); }); + it('should reject a truncated PEM public key', async () => { + ScriptManager.shared.addResolver(async (scriptId) => { + return { + url: Script.getRemoteURL(`http://domain.ext/${scriptId}`), + verifyScriptSignature: 'strict', + publicKey: RSA_PUBLIC_KEY.replace('-----END PUBLIC KEY-----', ''), + }; + }); + + await expect( + ScriptManager.shared.resolveScript('src_App_js', 'main') + ).rejects.toThrow( + 'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.' + ); + }); + + it('should reject a PKCS#1 public key with RSA PUBLIC KEY markers', async () => { + ScriptManager.shared.addResolver(async (scriptId) => { + return { + url: Script.getRemoteURL(`http://domain.ext/${scriptId}`), + verifyScriptSignature: 'strict', + publicKey: PKCS1_RSA_PUBLIC_KEY, + }; + }); + + await expect( + ScriptManager.shared.resolveScript('src_App_js', 'main') + ).rejects.toThrow( + 'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.' + ); + }); + it('should reject a large malformed public key without excessive backtracking', async () => { ScriptManager.shared.addResolver(async (scriptId) => { return { @@ -418,12 +466,16 @@ describe('ScriptManagerAPI', () => { }); it('should allow public key override with surrounding whitespace', async () => { + const publicKeyWithWindowsLineEndings = RSA_PUBLIC_KEY.replaceAll( + '\n', + '\r\n' + ); + ScriptManager.shared.addResolver(async (scriptId) => { return { url: Script.getRemoteURL(`http://domain.ext/${scriptId}`), verifyScriptSignature: 'strict', - publicKey: - '\n -----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY----- \n', + publicKey: `\r\n ${publicKeyWithWindowsLineEndings} \r\n`, }; }); @@ -432,9 +484,7 @@ describe('ScriptManagerAPI', () => { 'main' ); - expect(script.locator.publicKey).toBe( - '-----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY-----' - ); + expect(script.locator.publicKey).toBe(publicKeyWithWindowsLineEndings); }); it('should resolve with body', async () => {