test(smoke): add the missing response.getBodyBuffer id-vs-bodyPath smoke test

Item 1's id-reload fix (commit 92b25cbf2) landed with unit coverage but no
Playwright smoke test, despite the audit plan requiring one. Adds it: a
zero-permission template tag looks up its own real response by id and a
"victim" response by id, then calls response.getBodyBuffer({ _id: own,
bodyPath: victim's }) — confirmed this returned the victim's content against
the id-reload logic reverted, and the caller's own content with it restored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kyle
2026-07-25 22:40:10 -04:00
parent f4efb472e2
commit 818c4bcead
2 changed files with 220 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
type: collection.insomnia.rest/5.0
schema_version: "5.1"
name: response.getBodyBuffer id scope collection
meta:
id: wrk_6c1d9f2a000000000000000000000010
created: 1751900000000
modified: 1751900000000
collection:
- url: localhost:4010/echo
name: self
meta:
id: req_6c1d9f2a000000000000000000000011
created: 1751900000001
modified: 1751900000001
isPrivate: false
sortKey: -1751900000001
method: POST
body:
mimeType: text/plain
text: SELF-CONTENT-MARKER-7f3a9c
headers:
- name: Content-Type
value: text/plain
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: localhost:4010/echo
name: victim
meta:
id: req_6c1d9f2a000000000000000000000012
created: 1751900000002
modified: 1751900000002
isPrivate: false
sortKey: -1751900000002
method: POST
body:
mimeType: text/plain
text: VICTIM-CONTENT-MARKER-9c1e4b
headers:
- name: Content-Type
value: text/plain
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: http://127.0.0.1:4010/echo
name: Sandbox Probe
meta:
id: req_6c1d9f2a000000000000000000000013
created: 1751900000003
modified: 1751900000003
isPrivate: false
sortKey: -1751900000003
method: GET
body:
mimeType: text/plain
text: |
{% getbodybufferidprobe 'req_6c1d9f2a000000000000000000000011', 'req_6c1d9f2a000000000000000000000012' %}
headers:
- name: Content-Type
value: text/plain
cookieJar:
name: Default Jar
meta:
id: jar_6c1d9f2a000000000000000000000014
created: 1751900000000
modified: 1751900000000
environments:
name: Base Environment
meta:
id: env_6c1d9f2a000000000000000000000015
created: 1751900000000
modified: 1751900000000
isPrivate: false

View File

@@ -0,0 +1,137 @@
import fs from 'node:fs';
import path from 'node:path';
import { expect, type Page } from '@playwright/test';
import { loadFixture } from '../../playwright/paths';
import { test } from '../../playwright/test';
// response.getBodyBuffer sits behind the models.read capability, which is granted to every template
// tag at baseline with no manifest declaration. When a caller supplies response._id, the handler
// re-loads that response server-side and reads only its own bodyPath — ignoring whatever bodyPath the
// caller supplied alongside it. This closes a residual gap: pairing a real id (satisfying the
// pre-fix, bodyPath-only ownership check, which merely asks "does this bodyPath belong to *some*
// response") with a *different*, real response's bodyPath used to still read that other response's
// content (CROSS-TENANT-DB-ACCESS-FINDINGS.md Finding 5's write-up, closed by SEC-SERVICES-FIXES-AUDIT-PLAN.md
// Item 1).
const PLUGIN_NAME = 'insomnia-plugin-getbodybuffer-id-scope';
const SELF_MARKER = 'SELF-CONTENT-MARKER-7f3a9c';
const VICTIM_MARKER = 'VICTIM-CONTENT-MARKER-9c1e4b';
// Installs a template tag with no manifest permissions that looks up its own response by id (via the
// baseline-granted response.getLatestForRequestId) and its "victim" response by id, then calls
// response.getBodyBuffer with its own real id paired with the victim's real bodyPath.
const installIdScopeProbePlugin = (dataPath: string) => {
const pluginDir = path.join(dataPath, 'plugins', PLUGIN_NAME);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'package.json'),
JSON.stringify({ name: PLUGIN_NAME, version: '1.0.0', main: 'index.js', insomnia: {} }),
);
fs.writeFileSync(
path.join(pluginDir, 'index.js'),
`
module.exports.templateTags = [
{
name: 'getbodybufferidprobe',
displayName: 'GetBodyBuffer Id Probe',
description: 'Reads its own response by id while pairing it with a different, real response bodyPath',
args: [
{ displayName: 'Self Request Id', type: 'string', defaultValue: '' },
{ displayName: 'Victim Request Id', type: 'string', defaultValue: '' },
],
async run(context, selfRequestId, victimRequestId) {
var selfResp = await context.util.models.response.getLatestForRequestId(selfRequestId, null);
var victimResp = await context.util.models.response.getLatestForRequestId(victimRequestId, null);
if (!selfResp || !victimResp) { return 'MISSING_RESPONSE'; }
var raw = await context.util.models.response.getBodyBuffer({ _id: selfResp._id, bodyPath: victimResp.bodyPath });
var bytes = (raw && raw.data) || [];
var out = '';
for (var i = 0; i < bytes.length; i++) { out += String.fromCharCode(bytes[i]); }
return out;
},
},
];
`,
);
};
const clearPluginToast = async (page: Page) => {
await page.getByLabel('Import').waitFor();
await page.evaluate(() => localStorage.setItem('plugin-system-changes-toast-shown', 'true'));
const dismissButtons = page.getByRole('button', { name: 'Dismiss' });
await dismissButtons
.first()
.waitFor({ timeout: 3000 })
.catch(() => {});
const deadline = Date.now() + 5000;
while (Date.now() < deadline && (await dismissButtons.count()) > 0) {
await dismissButtons.first().click({ force: true, timeout: 500 }).catch(() => {});
}
};
// Enables the sandbox via the real Preferences → Scripting UI toggle, not a pre-set settings object.
const enableSandbox = async (page: Page) => {
await page.getByTestId('settings-button').click();
const sandboxToggle = page.getByTestId('toggle-template-tag-sandbox');
await page.getByRole('tab', { name: 'Scripting' }).click();
await sandboxToggle.getByRole('switch').waitFor();
await sandboxToggle.click();
await expect.soft(sandboxToggle.getByRole('switch')).toBeChecked();
await page.locator('.app').press('Escape');
await expect.soft(page.getByTestId('toggle-template-tag-sandbox')).toBeHidden();
};
const sendRequestAndWaitForResponse = async (page: Page, insomnia: any, requestName: string) => {
await insomnia.navigationSidebar.clickRequestOrFolder(requestName);
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.getByRole('button', { name: 'Cancel Request' })).toBeHidden({ timeout: 30_000 });
};
test('response.getBodyBuffer reads only the id-resolved response body, ignoring a paired other-response bodyPath', async ({
page,
app,
dataPath,
insomnia,
}) => {
installIdScopeProbePlugin(dataPath);
await clearPluginToast(page);
const fixture = await loadFixture('response-getbodybuffer-id-scope-collection.yaml');
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), fixture);
await page.getByLabel('Import').click();
await page.locator('[data-test-id="import-from-clipboard"]').click();
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
await page.evaluate(() => (window as any).main.plugins.reloadPlugins());
// Persist two real, distinct responses so the probe tag has real ids/bodyPaths to reference.
await sendRequestAndWaitForResponse(page, insomnia, 'self');
await sendRequestAndWaitForResponse(page, insomnia, 'victim');
await insomnia.navigationSidebar.clickRequestOrFolder('Sandbox Probe');
await page.getByText('Body', { exact: true }).click();
await enableSandbox(page);
const readTagPreview = async (tagPrefix: string): Promise<string> => {
await page.locator(`[data-template^="${tagPrefix}"]`).click();
const modal = page.getByRole('dialog');
const preview = modal.getByLabel('Live Preview');
await expect.soft(preview).not.toHaveValue('rendering...');
const value = (await preview.inputValue()).trim();
await modal.getByRole('button', { name: 'Done' }).click();
await expect.soft(modal).toBeHidden();
return value;
};
// The just-toggled setting needs a moment to propagate to the main process before the tag runs in
// the sandbox — poll until it does, then assert the caller's own response content is returned, not
// the paired victim response's. The echo server wraps the body in a JSON envelope (method/headers/
// data/cookies), so check for the marker as a substring rather than an exact match.
await expect.poll(() => readTagPreview('{% getbodybufferidprobe'), { timeout: 20_000 }).toContain(SELF_MARKER);
const finalPreview = await readTagPreview('{% getbodybufferidprobe');
expect.soft(finalPreview).not.toContain(VICTIM_MARKER);
});