mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-06 21:14:38 -04:00
fix: constrain file template tag (#10528)
* Narrow secureReadFile's userData allowlist to responses/ and version-control/
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user