mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-03 03:15:06 -04:00
fix(sandbox): re-verify response.getBodyBuffer against the id-resolved response, not the caller's bodyPath
assertResponseBodyPathReadOwnership only confirmed a supplied bodyPath belonged to *some* persisted response, so a caller supplying a real response id alongside a different, real response's bodyPath still read that other response's body. When an id is supplied, re-load the response server-side and read only its own bodyPath, ignoring the caller's; fall back to the existing bodyPath-ownership check when no id is available (the pre-persistence response-hook call site). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ vi.mock('insomnia-data', () => ({
|
||||
workspace: { getById: vi.fn() },
|
||||
oAuth2Token: { getByParentId: vi.fn() },
|
||||
cookieJar: { getOrCreateForParentId: vi.fn() },
|
||||
response: { getLatestForRequestId: vi.fn() },
|
||||
response: { getLatestForRequestId: vi.fn(), getById: vi.fn(), getByBodyPath: vi.fn() },
|
||||
helpers: { getResponseBodyBuffer: vi.fn() },
|
||||
settings: { get: vi.fn() },
|
||||
},
|
||||
@@ -257,6 +257,58 @@ describe('runPluginTagInSandbox — util.render escape', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('response.getBodyBuffer reads only the id-resolved response body when an id is supplied', () => {
|
||||
const runTag = (runBody: string) =>
|
||||
runPluginTagInSandbox(
|
||||
`module.exports.templateTags = [{ name: 't', run: async function (context) { ${runBody} } }];`,
|
||||
{
|
||||
args: [],
|
||||
pluginName: 'p',
|
||||
tagName: 't',
|
||||
context: { meta: {}, renderPurpose: 'send' as const, context: {} as any },
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
(services.helpers.getResponseBodyBuffer as any).mockImplementation(async (resp: any) => `read:${resp?.bodyPath}`);
|
||||
});
|
||||
|
||||
it('ignores a bodyPath belonging to a different response once an id is supplied', async () => {
|
||||
(services.response.getById as any).mockResolvedValue({ _id: 'r1', bodyPath: '/app/r1/body', bodyCompression: null });
|
||||
(services.response.getByBodyPath as any).mockResolvedValue({ _id: 'r2', parentId: 'req2', bodyPath: '/app/r2/body' });
|
||||
const result = await runTag(
|
||||
"return await context.util.models.response.getBodyBuffer({ _id: 'r1', bodyPath: '/app/r2/body' });",
|
||||
);
|
||||
expect(services.response.getById).toHaveBeenCalledWith('r1');
|
||||
expect(result).toBe('read:/app/r1/body');
|
||||
});
|
||||
|
||||
it('returns the read-failure value (never touches disk) when the supplied id is unknown', async () => {
|
||||
(services.response.getById as any).mockResolvedValue(null);
|
||||
(services.helpers.getResponseBodyBuffer as any).mockClear();
|
||||
const result = await runTag(
|
||||
"return await context.util.models.response.getBodyBuffer({ _id: 'unknown', bodyPath: '/app/r2/body' }, 'FAIL');",
|
||||
);
|
||||
expect(result).toBe('FAIL');
|
||||
expect(services.helpers.getResponseBodyBuffer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to bodyPath-ownership verification when no id is supplied (e.g. the pre-persistence hook path)', async () => {
|
||||
(services.response.getByBodyPath as any).mockResolvedValue({ _id: 'r2', parentId: 'req2', bodyPath: '/app/r2/body' });
|
||||
const result = await runTag(
|
||||
"return await context.util.models.response.getBodyBuffer({ bodyPath: '/app/r2/body' });",
|
||||
);
|
||||
expect(result).toBe('read:/app/r2/body');
|
||||
});
|
||||
|
||||
it('rejects a bodyPath that belongs to no known response when no id is supplied', async () => {
|
||||
(services.response.getByBodyPath as any).mockResolvedValue(null);
|
||||
await expect(
|
||||
runTag("return await context.util.models.response.getBodyBuffer({ bodyPath: '/outside/body' });"),
|
||||
).rejects.toThrow(/does not belong to any known response/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloudCredential.update reloads by id and strips identity fields from the patch', () => {
|
||||
const CREDS = ['render', 'models.read', 'util', 'crypto', 'credentials'];
|
||||
const BASELINE = ['render', 'models.read', 'util', 'crypto'];
|
||||
|
||||
@@ -667,9 +667,21 @@ export const pluginToMainAPI: Record<PluginToMainAPIPaths, (...args: any[]) => P
|
||||
return await services.response.getLatestForRequestId(body.requestId, body.environmentId);
|
||||
},
|
||||
'response.getBodyBuffer': async (body: {
|
||||
response?: { bodyPath?: string; bodyCompression?: any };
|
||||
response?: { _id?: string; bodyPath?: string; bodyCompression?: any };
|
||||
readFailureValue?: string;
|
||||
}) => {
|
||||
const id = body.response?._id;
|
||||
if (id) {
|
||||
// Stronger path: re-load the response by id and read only its own server-owned bodyPath,
|
||||
// ignoring whatever bodyPath the caller supplied alongside the id.
|
||||
const real = await services.response.getById(String(id));
|
||||
if (!real) {
|
||||
return body.readFailureValue ?? '';
|
||||
}
|
||||
return await services.helpers.getResponseBodyBuffer(real, body.readFailureValue);
|
||||
}
|
||||
// No id available (e.g. a response hook running before its response is persisted) — fall back to
|
||||
// verifying the supplied bodyPath belongs to some already-persisted response.
|
||||
await assertResponseBodyPathReadOwnership(body.response?.bodyPath);
|
||||
return await services.helpers.getResponseBodyBuffer(body.response, body.readFailureValue);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user