fix: constrain file template tag (#10528)

* Narrow secureReadFile's userData allowlist to responses/ and version-control/
This commit is contained in:
kwburns-kong authored and GitHub committed 2026-09-24 14:17:19 +00:00
1 parent bcaac3c08a
commit ae09eea24d
2 files changed
+66 -5

No files matched your search

@@ -39,7 +39,7 @@ describe('secureReadFile', () => {
fs.rmSync(TEST_ROOT, { recursive: true, force: true });
});
it('rejects a NeDB database file inside the allowed userData directory', async () => {
it('rejects a NeDB database file inside the userData directory', async () => {
const { secureReadFile } = await import('../secure-read-file');
const dbFile = path.join(userDataDir, 'insomnia.Environment.db');
fs.writeFileSync(dbFile, '{"secret":"vault-value"}');
@@ -47,12 +47,66 @@ describe('secureReadFile', () => {
await expect(secureReadFile(dbFile)).rejects.toThrow(/cannot access/);
});
it('allows an ordinary file inside the allowed userData directory', async () => {
it('rejects a NeDB-named file placed inside the allowed responses subdirectory', async () => {
// isReservedDatabaseFile matches by basename alone, so it still guards the allowed
// responses/version-control subdirectories even though real NeDB files never live there.
const { secureReadFile } = await import('../secure-read-file');
const responsesDir = path.join(userDataDir, 'responses');
fs.mkdirSync(responsesDir, { recursive: true });
const dbFile = path.join(responsesDir, 'insomnia.Environment.db');
fs.writeFileSync(dbFile, '{"secret":"vault-value"}');
await expect(secureReadFile(dbFile)).rejects.toThrow(/cannot access/);
});
it('rejects an ordinary file directly inside the userData directory', async () => {
// Only responses/ and version-control/ subdirectories are allowed roots; the rest of
// userData (cookies, caches, etc.) is out of scope for this file-read surface.
const { secureReadFile } = await import('../secure-read-file');
const file = path.join(userDataDir, 'notes.txt');
fs.writeFileSync(file, 'hello');
await expect(secureReadFile(file)).resolves.toBe('hello');
await expect(secureReadFile(file)).rejects.toThrow(/cannot access/);
});
it('allows an ordinary file inside the userData responses directory', async () => {
const { secureReadFile } = await import('../secure-read-file');
const responsesDir = path.join(userDataDir, 'responses');
fs.mkdirSync(responsesDir, { recursive: true });
const file = path.join(responsesDir, 'abc123.response');
fs.writeFileSync(file, 'response-body');
await expect(secureReadFile(file)).resolves.toBe('response-body');
});
it('allows an ordinary file inside the userData version-control directory', async () => {
const { secureReadFile } = await import('../secure-read-file');
const vcsDir = path.join(userDataDir, 'version-control', 'projects', 'prj_1');
fs.mkdirSync(vcsDir, { recursive: true });
const file = path.join(vcsDir, 'head.json');
fs.writeFileSync(file, '{"branch":"main"}');
await expect(secureReadFile(file)).resolves.toBe('{"branch":"main"}');
});
it('rejects a Chromium cookie store file directly inside the userData directory', async () => {
const { secureReadFile } = await import('../secure-read-file');
const cookiesFile = path.join(userDataDir, 'Cookies');
fs.writeFileSync(cookiesFile, 'sqlite-cookie-data');
await expect(secureReadFile(cookiesFile)).rejects.toThrow(/cannot access/);
});
it('rejects a sibling directory that merely shares a userData subdirectory name as a string prefix', async () => {
const { secureReadFile } = await import('../secure-read-file');
const responsesDir = path.join(userDataDir, 'responses');
fs.mkdirSync(responsesDir, { recursive: true });
const siblingDir = `${responsesDir}Other`;
fs.mkdirSync(siblingDir);
const file = path.join(siblingDir, 'notes.txt');
fs.writeFileSync(file, 'hello');
await expect(secureReadFile(file)).rejects.toThrow(/cannot access/);
});
it('rejects a sibling directory that merely shares the allowed directory as a string prefix', async () => {
@@ -21,12 +21,19 @@ export const isPathAllowed = (filePath: string, userAllowList: string[]) => {
return { isAllowed, securedPath };
};
const securePath = (filePath: string) => path.resolve(decodeURIComponent(filePath));
// Only responses/ and version-control/ subdirectories are in scope for this file-read surface;
// the rest of userData (cookies, caches, etc.) is Electron/Chromium's own runtime state, not
// something templates, plugins, or scripts have a reason to read.
const USERDATA_ALLOWED_SUBDIRS = ['responses', 'version-control'];
const getSecuredFolderAllowList = (userAllowList: string[]) => {
const userdataDirectory = process.env.INSOMNIA_DATA_PATH || electron.app.getPath('userData');
// we use tmpdir for buildMultipart
// we put the db in userData
// the user can also specifiy other folders
return [os.tmpdir(), userdataDirectory, ...userAllowList];
return [
os.tmpdir(),
...USERDATA_ALLOWED_SUBDIRS.map(subdir => path.join(userdataDirectory, subdir)),
...userAllowList,
];
};
// NeDB stores every model as `insomnia.<Model>.db` directly inside the userData directory
// (see database-nedb.ts), so the templating/plugin/script file-read surface must never be