Skip to content

Commit 5ca584b

Browse files
fabriziocuccimeta-codesync[bot]
authored andcommitted
Validate assets against file map before serving (#1643)
Summary: Pull Request resolved: #1643 Assets served via the `/assets/` endpoint were not being validated against Metro's file map. This meant that files excluded from the file map (via `blockList`, `.git`/`.hg` directories, etc.) could still be accessed through the asset serving endpoint, potentially exposing sensitive files. This change adds file map validation to `getAsset()` by accepting an optional file existence check function. The Server now passes the DependencyGraph's `doesFileExist` method, which checks whether a file is present in the file map. Assets not in the file map are rejected with an appropriate error message. This approach is more robust than checking blockList directly because the file map already applies all filtering logic (blockList patterns, VCS directories like `.git`/`.hg`, etc.), ensuring assets follow the same visibility rules as modules. Reviewed By: robhogan Differential Revision: D91128421 fbshipit-source-id: 0721599a609a54f02b2ff9f1474fe33726b8ee5e
1 parent 947bac8 commit 5ca584b

6 files changed

Lines changed: 40 additions & 7 deletions

File tree

docs/Configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ If set to `false`, prevents Metro from using Watchman (even if it's installed).
302302
303303
Type: `RegExp` or `Array<RegExp>`
304304
305-
A regular expression (or list of regular expressions) defining which paths to exclude from Metro's file map. Files whose absolute paths match these patterns are effectively hidden from Metro and cannot be resolved or imported in the current project.
305+
A regular expression (or list of regular expressions) defining which paths to exclude from Metro's file map. Files whose absolute paths match these patterns are effectively hidden from Metro and cannot be resolved or imported in the current project. Additionally, blocked files cannot be served via the `/assets/` endpoint.
306306
307307
#### `hasteImplModulePath`
308308

packages/metro/src/Assets.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export async function getAsset(
282282
watchFolders: ReadonlyArray<string>,
283283
platform: ?string = null,
284284
assetExts: ReadonlyArray<string>,
285+
fileExistsInFileMap?: (absolutePath: string) => boolean,
285286
): Promise<Buffer> {
286287
const assetData = AssetPaths.parse(
287288
relativePath,
@@ -296,10 +297,19 @@ export async function getAsset(
296297
);
297298
}
298299

299-
if (!pathBelongsToRoots(absolutePath, [projectRoot, ...watchFolders])) {
300-
throw new Error(
301-
`'${relativePath}' could not be found, because it cannot be found in the project root or any watch folder`,
302-
);
300+
// NOTE: If fileExistsInFileMap is not provided, we fall back to pathBelongsToRoots for backward compatibility, as getAsset is part of the public API.
301+
if (fileExistsInFileMap != null) {
302+
if (!fileExistsInFileMap(absolutePath)) {
303+
throw new Error(
304+
`'${relativePath}' could not be found, because it is not within the projectRoot or watchFolders, or it is blocked via the resolver.blockList config`,
305+
);
306+
}
307+
} else {
308+
if (!pathBelongsToRoots(absolutePath, [projectRoot, ...watchFolders])) {
309+
throw new Error(
310+
`'${relativePath}' could not be found, because it cannot be found in the project root or any watch folder`,
311+
);
312+
}
303313
}
304314

305315
const record = await getAbsoluteAssetRecord(absolutePath, platform);

packages/metro/src/Server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,12 +549,14 @@ export default class Server {
549549
);
550550

551551
try {
552+
const depGraph = await this._bundler.getBundler().getDependencyGraph();
552553
const data = await getAsset(
553554
assetPath,
554555
this._config.projectRoot,
555556
this._config.watchFolders,
556557
urlObj.searchParams.get('platform'),
557558
this._config.resolver.assetExts,
559+
filePath => depGraph.doesFileExist(filePath),
558560
);
559561
// Tell clients to cache this for 1 year.
560562
// This is safe as the asset url contains a hash of the asset.

packages/metro/src/Server/__tests__/Server-test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ describe('processRequest', () => {
839839
['/root'],
840840
'ios',
841841
expect.any(Array),
842+
expect.any(Function),
842843
);
843844
});
844845

@@ -856,6 +857,7 @@ describe('processRequest', () => {
856857
['/root'],
857858
'ios',
858859
expect.any(Array),
860+
expect.any(Function),
859861
);
860862
expect(response._getString()).toBe(mockData.slice(0, 4));
861863
});
@@ -912,6 +914,7 @@ describe('processRequest', () => {
912914
['/root'],
913915
null,
914916
expect.any(Array),
917+
expect.any(Function),
915918
);
916919
});
917920

@@ -936,6 +939,7 @@ describe('processRequest', () => {
936939
['/root'],
937940
'ios',
938941
expect.any(Array),
942+
expect.any(Function),
939943
);
940944
expect(response._getString()).toBe('i am image');
941945
});
@@ -953,6 +957,7 @@ describe('processRequest', () => {
953957
['/root'],
954958
null,
955959
expect.any(Array),
960+
expect.any(Function),
956961
);
957962
expect(response._getString()).toBe('i am image');
958963
});

packages/metro/src/__tests__/Assets-test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ describe('getAsset', () => {
150150
getAssetStr('../anotherfolder/b.png', '/root', [], null, ['png']),
151151
).rejects.toBeInstanceOf(Error);
152152
});
153+
154+
test('should find an image when fileExistsInFileMap returns true', async () => {
155+
writeImages({'b.png': 'b image'});
156+
157+
expect(
158+
await getAssetStr('imgs/b.png', '/root', [], null, ['png'], () => true),
159+
).toBe('b image');
160+
});
161+
162+
test('should throw an error when fileExistsInFileMap returns false', async () => {
163+
writeImages({'b.png': 'b image'});
164+
165+
await expect(
166+
getAssetStr('imgs/b.png', '/root', [], null, ['png'], () => false),
167+
).rejects.toBeInstanceOf(Error);
168+
});
153169
});
154170

155171
describe('getAssetData', () => {

packages/metro/src/node-haste/DependencyGraph.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export default class DependencyGraph extends EventEmitter {
178178
},
179179
disableHierarchicalLookup:
180180
this._config.resolver.disableHierarchicalLookup,
181-
doesFileExist: this._doesFileExist,
181+
doesFileExist: this.doesFileExist,
182182
emptyModulePath: this._config.resolver.emptyModulePath,
183183
extraNodeModules: this._config.resolver.extraNodeModules,
184184
fileSystemLookup,
@@ -363,7 +363,7 @@ export default class DependencyGraph extends EventEmitter {
363363
return resolution;
364364
}
365365

366-
_doesFileExist = (filePath: string): boolean => {
366+
doesFileExist = (filePath: string): boolean => {
367367
return this._fileSystem.exists(filePath);
368368
};
369369

0 commit comments

Comments
 (0)