diff --git a/packages/insomnia/src/main/__tests__/templating-worker-database-protocol-authorization.test.ts b/packages/insomnia/src/main/__tests__/templating-worker-database-protocol-authorization.test.ts index d12e969f82..7c23b1eee5 100644 --- a/packages/insomnia/src/main/__tests__/templating-worker-database-protocol-authorization.test.ts +++ b/packages/insomnia/src/main/__tests__/templating-worker-database-protocol-authorization.test.ts @@ -514,6 +514,19 @@ describe('plugin.runUserResponseHook: setBody cannot redirect its write onto a d expect(fs.readFileSync(otherBodyPath, 'utf8')).toBe('other-original-body'); }); + it('rejects a bodyPath reached through a symlinked directory that resolves outside the responses directory', async () => { + const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'insomnia-outside-')); + const linkedDir = path.join(responsesDir, 'linked'); + fs.symlinkSync(outsideDir, linkedDir, 'dir'); + const throughSymlink = path.join(linkedDir, 'escaped-body.txt'); + try { + await runResponseHook(throughSymlink, 'req_1'); + expect(fs.existsSync(path.join(outsideDir, 'escaped-body.txt'))).toBe(false); + } finally { + fs.rmSync(outsideDir, { recursive: true, force: true }); + } + }); + // Same check, reached by sending the path-normalization variant directly to response.setBody. it('rejects a path-normalization variant sent directly to response.setBody', async () => { const { services } = await import('insomnia-data'); @@ -539,6 +552,34 @@ describe('plugin.runUserResponseHook: setBody cannot redirect its write onto a d expect(json.error).toMatch(/belongs to a different response/); expect(fs.readFileSync(otherBodyPath, 'utf8')).toBe('other-original-body'); }); + + // Same check, reached by sending a bodyPath through a symlinked directory directly to response.setBody. + it('rejects a bodyPath through a symlinked directory sent directly to response.setBody', async () => { + const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'insomnia-outside-')); + const linkedDir = path.join(responsesDir, 'linked'); + fs.symlinkSync(outsideDir, linkedDir, 'dir'); + const throughSymlink = path.join(linkedDir, 'escaped-body.txt'); + const token = getOrCreateTemplatingDbAuthToken(); + const { resolveDbByKey } = await import('../templating-worker-database'); + try { + const req = new Request('insomnia-templating-worker-database://response.setbody', { + method: 'POST', + headers: { [TEMPLATING_DB_AUTH_HEADER]: token }, + body: JSON.stringify({ + bodyPath: throughSymlink, + bodyBase64: Buffer.from('replacement-body', 'utf8').toString('base64'), + parentId: 'req_1', + }), + }); + const res = await resolveDbByKey(req); + expect(res.status).toBe(500); + const json = await res.json(); + expect(json.error).toMatch(/escapes the responses directory/); + expect(fs.existsSync(path.join(outsideDir, 'escaped-body.txt'))).toBe(false); + } finally { + fs.rmSync(outsideDir, { recursive: true, force: true }); + } + }); }); describe('discoverPluginExportsInSandbox strips dangerous JSON keys, symmetric with the hook path', () => { diff --git a/packages/insomnia/src/main/templating-worker-database.ts b/packages/insomnia/src/main/templating-worker-database.ts index b05210d5f0..0f48408248 100644 --- a/packages/insomnia/src/main/templating-worker-database.ts +++ b/packages/insomnia/src/main/templating-worker-database.ts @@ -707,6 +707,12 @@ export const pluginToMainAPI: Record P if (relative.startsWith('..') || path.isAbsolute(relative)) { throw new Error('response.setBody path escapes the responses directory'); } + // Re-check after resolving symlinks, mirroring getPluginEntrySource/readPluginModuleMap: a + // symlinked directory entry under responsesDir could otherwise resolve outside it despite the + // string-based check above passing. + if (!isContainedIn(fs.realpathSync(responsesDir), fs.realpathSync(path.dirname(target)))) { + throw new Error('response.setBody path escapes the responses directory'); + } fs.writeFileSync(target, Buffer.from(body.bodyBase64, 'base64')); return null; },