From 5194faa4c81ef2990e5fede14a3932bf9edd7489 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 25 Jun 2026 19:13:40 +0200 Subject: [PATCH 1/2] Fix asset root containment check --- packages/metro/src/Assets.js | 16 ++++++++- packages/metro/src/__tests__/Assets-test.js | 36 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/metro/src/Assets.js b/packages/metro/src/Assets.js index 2760bb786e..172af04432 100644 --- a/packages/metro/src/Assets.js +++ b/packages/metro/src/Assets.js @@ -341,11 +341,25 @@ function pathBelongsToRoots( pathToCheck: string, roots: ReadonlyArray, ): boolean { + const absolutePathToCheck = path.resolve(pathToCheck); + for (const rootFolder of roots) { - if (pathToCheck.startsWith(path.resolve(rootFolder))) { + if (isPathInsideRoot(absolutePathToCheck, rootFolder)) { return true; } } return false; } + +function isPathInsideRoot(pathToCheck: string, rootFolder: string): boolean { + const relativePath = path.relative(path.resolve(rootFolder), pathToCheck); + + // path.relative('/repo', '/repo2') -> '../repo2', so this must reject leading "..". + return ( + relativePath === '' || + (!relativePath.startsWith(`..${path.sep}`) && + relativePath !== '..' && + !path.isAbsolute(relativePath)) + ); +} diff --git a/packages/metro/src/__tests__/Assets-test.js b/packages/metro/src/__tests__/Assets-test.js index f010240f24..9178c283d7 100644 --- a/packages/metro/src/__tests__/Assets-test.js +++ b/packages/metro/src/__tests__/Assets-test.js @@ -151,6 +151,42 @@ describe('getAsset', () => { ).rejects.toBeInstanceOf(Error); }); + test('should reject sibling-prefix watchFolder paths outside watchFolders', async () => { + fs.mkdirSync('/watchfoldersub/imgs', {recursive: true}); + + fs.writeFileSync('/watchfoldersub/imgs/b.png', 'b image'); + + await expect( + getAssetStr( + '../watchfoldersub/imgs/b.png', + '/watchfolder', + ['/watchfolder/one'], + null, + ['png'], + ), + ).rejects.toBeInstanceOf(Error); + }); + + test('should reject sibling-prefix asset paths outside root', async () => { + fs.mkdirSync('/app2/imgs', {recursive: true}); + + fs.writeFileSync('/app2/imgs/b.png', 'b image'); + + await expect( + getAssetStr('../app2/imgs/b.png', '/app', [], null, ['png']), + ).rejects.toBeInstanceOf(Error); + }); + + test('should reject sibling-prefix asset paths outside root even with trailing root separator', async () => { + fs.mkdirSync('/app2/imgs', {recursive: true}); + + fs.writeFileSync('/app2/imgs/b.png', 'b image'); + + await expect( + getAssetStr('../app2/imgs/b.png', '/app/', [], null, ['png']), + ).rejects.toBeInstanceOf(Error); + }); + test('should find an image when fileExistsInFileMap returns true', async () => { writeImages({'b.png': 'b image'}); From 6f0ffea19ba4775322d596eca82a3c590dcc98c3 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 25 Jun 2026 19:27:42 +0200 Subject: [PATCH 2/2] fix: harden asset path containment check --- packages/metro/src/Assets.js | 10 +++- packages/metro/src/__tests__/Assets-test.js | 56 +++++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/metro/src/Assets.js b/packages/metro/src/Assets.js index 172af04432..9b8b343c3f 100644 --- a/packages/metro/src/Assets.js +++ b/packages/metro/src/Assets.js @@ -352,8 +352,14 @@ function pathBelongsToRoots( return false; } -function isPathInsideRoot(pathToCheck: string, rootFolder: string): boolean { - const relativePath = path.relative(path.resolve(rootFolder), pathToCheck); +function isPathInsideRoot( + absolutePathToCheck: string, + rootFolder: string, +): boolean { + const relativePath = path.relative( + path.resolve(rootFolder), + absolutePathToCheck, + ); // path.relative('/repo', '/repo2') -> '../repo2', so this must reject leading "..". return ( diff --git a/packages/metro/src/__tests__/Assets-test.js b/packages/metro/src/__tests__/Assets-test.js index 9178c283d7..8274f6d366 100644 --- a/packages/metro/src/__tests__/Assets-test.js +++ b/packages/metro/src/__tests__/Assets-test.js @@ -152,15 +152,19 @@ describe('getAsset', () => { }); test('should reject sibling-prefix watchFolder paths outside watchFolders', async () => { - fs.mkdirSync('/watchfoldersub/imgs', {recursive: true}); + const outsideWatchFolder = path.resolve('/watchfoldersub'); + const insideWatchFolder = path.resolve('/watchfolder'); + const childRoot = path.join(outsideWatchFolder, 'imgs'); + const relativePathToAsset = path.join('..', 'watchfoldersub', 'imgs', 'b.png'); - fs.writeFileSync('/watchfoldersub/imgs/b.png', 'b image'); + fs.mkdirSync(childRoot, {recursive: true}); + fs.writeFileSync(path.join(childRoot, 'b.png'), 'b image'); await expect( getAssetStr( - '../watchfoldersub/imgs/b.png', - '/watchfolder', - ['/watchfolder/one'], + relativePathToAsset, + insideWatchFolder, + [path.join(insideWatchFolder, 'one')], null, ['png'], ), @@ -168,22 +172,52 @@ describe('getAsset', () => { }); test('should reject sibling-prefix asset paths outside root', async () => { - fs.mkdirSync('/app2/imgs', {recursive: true}); + const rootFolder = path.resolve('/app'); + const outsideRoot = path.resolve('/app2'); + const childRoot = path.join(outsideRoot, 'imgs'); - fs.writeFileSync('/app2/imgs/b.png', 'b image'); + fs.mkdirSync(childRoot, {recursive: true}); + fs.writeFileSync(path.join(childRoot, 'b.png'), 'b image'); await expect( - getAssetStr('../app2/imgs/b.png', '/app', [], null, ['png']), + getAssetStr( + path.join('..', 'app2', 'imgs', 'b.png'), + rootFolder, + [], + null, + ['png'], + ), ).rejects.toBeInstanceOf(Error); }); + test('should accept asset paths inside root', async () => { + const rootFolder = path.resolve('/app'); + const childRoot = path.join(rootFolder, 'imgs'); + + fs.mkdirSync(childRoot, {recursive: true}); + fs.writeFileSync(path.join(childRoot, 'b.png'), 'b image'); + + await expect( + getAssetStr('imgs/b.png', rootFolder, [], null, ['png']), + ).resolves.toContain('b image'); + }); + test('should reject sibling-prefix asset paths outside root even with trailing root separator', async () => { - fs.mkdirSync('/app2/imgs', {recursive: true}); + const rootFolder = path.resolve('/app'); + const outsideRoot = path.resolve('/app2'); + const childRoot = path.join(outsideRoot, 'imgs'); - fs.writeFileSync('/app2/imgs/b.png', 'b image'); + fs.mkdirSync(childRoot, {recursive: true}); + fs.writeFileSync(path.join(childRoot, 'b.png'), 'b image'); await expect( - getAssetStr('../app2/imgs/b.png', '/app/', [], null, ['png']), + getAssetStr( + path.join('..', 'app2', 'imgs', 'b.png'), + rootFolder + path.sep, + [], + null, + ['png'], + ), ).rejects.toBeInstanceOf(Error); });