Skip to content

Commit 1a94ba4

Browse files
ochafikclaude
andcommitted
test: add cross-app message injection protection test
Adds tests for the attack vector where a malicious app tries to inject messages into another app via: window.parent.parent.frames[i].frames[0].postMessage(fakeResponse, "*") The protection (added in PR #207) is that PostMessageTransport validates event.source matches the expected source (window.parent for apps), so messages from other apps are rejected. Tests added: 1. "app rejects messages from sources other than its parent" - Simulates injection attempt from page context - Verifies app remains functional after attack attempt 2. "PostMessageTransport is configured with source validation" - Verifies valid parent->app communication still works - Confirms source validation doesn't break legitimate messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 20ddee0 commit 1a94ba4

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

tests/e2e/security.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,100 @@ test.describe("Origin Validation Infrastructure", () => {
268268
});
269269
});
270270

271+
test.describe("Cross-App Message Injection Protection", () => {
272+
/**
273+
* This tests protection against the attack where a malicious app tries to
274+
* inject messages into another app via:
275+
* window.parent.parent.frames[i].frames[0].postMessage(fakeResponse, "*")
276+
*
277+
* The protection is that PostMessageTransport validates event.source matches
278+
* the expected source (window.parent for apps), so messages from other apps
279+
* are rejected.
280+
*/
281+
test("app rejects messages from sources other than its parent", async ({
282+
page,
283+
}) => {
284+
// Capture any "unknown source" rejection logs
285+
const rejectionLogs: string[] = [];
286+
page.on("console", (msg) => {
287+
const text = msg.text();
288+
if (
289+
text.includes("unknown source") ||
290+
text.includes("Ignoring message")
291+
) {
292+
rejectionLogs.push(text);
293+
}
294+
});
295+
296+
await loadServer(page, "Integration Test Server");
297+
298+
const appFrame = getAppFrame(page);
299+
await expect(appFrame.locator("body")).toBeVisible();
300+
301+
// Try to inject a message from the page context (simulating cross-app attack)
302+
// This simulates what would happen if another app tried to postMessage to this app
303+
await page.evaluate(() => {
304+
// Get reference to the inner app iframe
305+
const outerIframe = document.querySelector("iframe");
306+
if (!outerIframe?.contentWindow) return;
307+
308+
const innerIframe = outerIframe.contentDocument?.querySelector("iframe");
309+
if (!innerIframe?.contentWindow) return;
310+
311+
// Try to send a fake JSON-RPC message (simulating malicious app)
312+
// This should be rejected because event.source won't match window.parent
313+
innerIframe.contentWindow.postMessage(
314+
{
315+
jsonrpc: "2.0",
316+
result: { content: [{ type: "text", text: "Injected!" }] },
317+
id: 999,
318+
},
319+
"*",
320+
);
321+
});
322+
323+
// Wait for message to be processed
324+
await page.waitForTimeout(500);
325+
326+
// The injected message should have been rejected
327+
// (it won't cause visible harm even if not logged, but ideally we see rejection)
328+
// The app should still be functional (not corrupted by the injection)
329+
await expect(appFrame.locator("body")).toBeVisible();
330+
331+
// Verify legitimate communication still works after attempted injection
332+
const sendMessageBtn = appFrame.locator('button:has-text("Send Message")');
333+
if (await sendMessageBtn.isVisible()) {
334+
await sendMessageBtn.click();
335+
await page.waitForTimeout(300);
336+
// If we get here without errors, the app wasn't corrupted
337+
}
338+
});
339+
340+
test("PostMessageTransport is configured with source validation", async ({
341+
page,
342+
}) => {
343+
// This test verifies that the App's transport is set up correctly
344+
// by checking that valid parent->app communication works
345+
346+
await loadServer(page, "Integration Test Server");
347+
348+
const appFrame = getAppFrame(page);
349+
350+
// The app should receive messages from parent (valid source)
351+
// If source validation was broken, the app wouldn't work at all
352+
await expect(appFrame.locator("body")).toBeVisible();
353+
354+
// Trigger a host->app notification (resize, theme change, etc.)
355+
// by resizing the page - this sends a message from host to app
356+
await page.setViewportSize({ width: 800, height: 600 });
357+
await page.waitForTimeout(300);
358+
359+
// App should still be responsive
360+
const buttons = appFrame.locator("button");
361+
await expect(buttons.first()).toBeVisible();
362+
});
363+
});
364+
271365
test.describe("Security Self-Test", () => {
272366
test("sandbox security self-test passes (window.top inaccessible)", async ({
273367
page,

0 commit comments

Comments
 (0)