Skip to content

Commit 376c996

Browse files
authored
fix: prevent public key validation ReDoS (#1417)
1 parent b63a6b5 commit 376c996

3 files changed

Lines changed: 81 additions & 10 deletions

File tree

.changeset/calm-keys-verify.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@callstack/repack": patch
3+
---
4+
5+
Prevent excessive regular-expression backtracking when validating per-script public keys.

packages/repack/src/modules/ScriptManager/__tests__/ScriptManager.test.ts

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { createPublicKey } from 'node:crypto';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
14
import NativeScriptManager, {
25
type NormalizedScriptLocator,
36
} from '../NativeScriptManager.js';
@@ -31,6 +34,21 @@ webpackRequire.repack = {
3134

3235
globalThis.__webpack_require__ = webpackRequire;
3336

37+
const RSA_PUBLIC_KEY = fs
38+
.readFileSync(
39+
path.join(
40+
__dirname,
41+
'../../../plugins/__tests__/__fixtures__/testRS256.pem.pub'
42+
),
43+
'utf8'
44+
)
45+
.trim();
46+
47+
const PKCS1_RSA_PUBLIC_KEY = createPublicKey(RSA_PUBLIC_KEY)
48+
.export({ format: 'pem', type: 'pkcs1' })
49+
.toString()
50+
.trim();
51+
3452
class FakeCache {
3553
data: Record<string, string> = {};
3654

@@ -362,8 +380,7 @@ describe('ScriptManagerAPI', () => {
362380
return {
363381
url: Script.getRemoteURL(`http://domain.ext/${scriptId}`),
364382
verifyScriptSignature: 'strict',
365-
publicKey:
366-
'-----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY-----',
383+
publicKey: RSA_PUBLIC_KEY,
367384
};
368385
});
369386

@@ -379,8 +396,7 @@ describe('ScriptManagerAPI', () => {
379396
method: 'GET',
380397
timeout: Script.DEFAULT_TIMEOUT,
381398
verifyScriptSignature: 'strict',
382-
publicKey:
383-
'-----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY-----',
399+
publicKey: RSA_PUBLIC_KEY,
384400
uniqueId: 'main_src_App_js',
385401
});
386402
});
@@ -401,13 +417,65 @@ describe('ScriptManagerAPI', () => {
401417
);
402418
});
403419

420+
it('should reject a truncated PEM public key', async () => {
421+
ScriptManager.shared.addResolver(async (scriptId) => {
422+
return {
423+
url: Script.getRemoteURL(`http://domain.ext/${scriptId}`),
424+
verifyScriptSignature: 'strict',
425+
publicKey: RSA_PUBLIC_KEY.replace('-----END PUBLIC KEY-----', ''),
426+
};
427+
});
428+
429+
await expect(
430+
ScriptManager.shared.resolveScript('src_App_js', 'main')
431+
).rejects.toThrow(
432+
'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.'
433+
);
434+
});
435+
436+
it('should reject a PKCS#1 public key with RSA PUBLIC KEY markers', async () => {
437+
ScriptManager.shared.addResolver(async (scriptId) => {
438+
return {
439+
url: Script.getRemoteURL(`http://domain.ext/${scriptId}`),
440+
verifyScriptSignature: 'strict',
441+
publicKey: PKCS1_RSA_PUBLIC_KEY,
442+
};
443+
});
444+
445+
await expect(
446+
ScriptManager.shared.resolveScript('src_App_js', 'main')
447+
).rejects.toThrow(
448+
'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.'
449+
);
450+
});
451+
452+
it('should reject a large malformed public key without excessive backtracking', async () => {
453+
ScriptManager.shared.addResolver(async (scriptId) => {
454+
return {
455+
url: Script.getRemoteURL(`http://domain.ext/${scriptId}`),
456+
verifyScriptSignature: 'strict',
457+
publicKey: `-----BEGIN PUBLIC KEY-----${' '.repeat(4096)}x`,
458+
};
459+
});
460+
461+
await expect(
462+
ScriptManager.shared.resolveScript('src_App_js', 'main')
463+
).rejects.toThrow(
464+
'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.'
465+
);
466+
});
467+
404468
it('should allow public key override with surrounding whitespace', async () => {
469+
const publicKeyWithWindowsLineEndings = RSA_PUBLIC_KEY.replaceAll(
470+
'\n',
471+
'\r\n'
472+
);
473+
405474
ScriptManager.shared.addResolver(async (scriptId) => {
406475
return {
407476
url: Script.getRemoteURL(`http://domain.ext/${scriptId}`),
408477
verifyScriptSignature: 'strict',
409-
publicKey:
410-
'\n -----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY----- \n',
478+
publicKey: `\r\n ${publicKeyWithWindowsLineEndings} \r\n`,
411479
};
412480
});
413481

@@ -416,9 +484,7 @@ describe('ScriptManagerAPI', () => {
416484
'main'
417485
);
418486

419-
expect(script.locator.publicKey).toBe(
420-
'-----BEGIN PUBLIC KEY-----\\ncustom\\n-----END PUBLIC KEY-----'
421-
);
487+
expect(script.locator.publicKey).toBe(publicKeyWithWindowsLineEndings);
422488
});
423489

424490
it('should resolve with body', async () => {

packages/repack/src/modules/ScriptManager/normalizePublicKey.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NormalizedScriptLocatorSignatureVerificationMode } from './NativeScriptManager.js';
22

33
const PUBLIC_KEY_PEM_PATTERN =
4-
/^-----BEGIN PUBLIC KEY-----\s*[\s\S]+?\s*-----END PUBLIC KEY-----$/;
4+
/^-----BEGIN PUBLIC KEY-----[\s\S]+-----END PUBLIC KEY-----$/;
55

66
export const INVALID_PUBLIC_KEY_ERROR =
77
'Property publicKey must be a PEM-formatted public key enclosed in BEGIN/END PUBLIC KEY markers.';

0 commit comments

Comments
 (0)