ai-assistant: docs/proposals: Add consent-poc adversarial testing harness#890
Open
illume wants to merge 1 commit into
Open
ai-assistant: docs/proposals: Add consent-poc adversarial testing harness#890illume wants to merge 1 commit into
illume wants to merge 1 commit into
Conversation
e214bd1 to
05b9c14
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a consent/approval-flow proof-of-concept under ai-assistant/docs/proposals/consent-poc/, including a protected Node.js server, a deliberately vulnerable twin server used as a negative control, and a Playwright E2E/adversarial test harness to validate the security properties (Sec-Fetch filtering, COOP isolation, nonce validation, SW registration blocking).
Changes:
- Add a dependency-free Node HTTP PoC server (
server.cjs) implementing the consent popup flow and server-side protections. - Add a vulnerable “twin” server (
vulnerable-server.cjs) to validate that attacks truly succeed when protections are removed. - Add Playwright config + two test suites (positive + adversarial/negative-control), plus documentation and local npm scripts.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ai-assistant/docs/proposals/consent-poc/server.cjs | Protected PoC server implementing consent flow, COOP header, Sec-Fetch gating, SW header blocking, and nonce approval. |
| ai-assistant/docs/proposals/consent-poc/vulnerable-server.cjs | Vulnerable twin server used as a negative control for adversarial tests. |
| ai-assistant/docs/proposals/consent-poc/tests/consent-poc.pw-spec.ts | Core Playwright suite validating HTTP protections and end-to-end popup behavior. |
| ai-assistant/docs/proposals/consent-poc/tests/consent-poc-adversarial.pw-spec.ts | Adversarial/negative-control suite (raw-socket checks, fuzzing, twin validation, popup isolation assertions). |
| ai-assistant/docs/proposals/consent-poc/playwright.config.ts | Playwright configuration and dual webServer startup (protected + twin). |
| ai-assistant/docs/proposals/consent-poc/README.md | Documentation for the PoC model, threat vectors, and test harness usage. |
| ai-assistant/docs/proposals/consent-poc/package.json | Local scripts and Playwright dev dependency for running the E2E suite. |
| ai-assistant/docs/proposals/consent-poc/.gitignore | Local ignores for node/test artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Minimal proof of concept for the same-port consent popup security model described in | ||
| [`backend-and-mcp.md`](../backend-and-mcp.md). | ||
|
|
||
| **No third-party dependencies.** Pure Node.js + minimal HTML/JS. |
| ## Quick start | ||
|
|
||
| ```bash | ||
| cd docs/proposals/consent-poc |
| ## What it proves | ||
|
|
||
| The PoC demonstrates that a **same-port popup** can be securely isolated from the | ||
| page that opens it, using three coordinated browser mechanisms: |
Comment on lines
+87
to
+90
| - `tests/consent-poc.spec.ts` — 19 tests covering the attack table above | ||
| (HTTP-level Sec-Fetch filtering, COOP header, nonce validation, nonce | ||
| replay, Service Worker blocking) and browser-level attack buttons. | ||
| - `tests/consent-poc-adversarial.spec.ts` — 34 additional tests that attack |
|
|
||
| // Adversarial / negative-control tests for the consent-poc. | ||
| // | ||
| // Rationale: the positive tests in consent-poc.spec.ts could theoretically |
Comment on lines
+32
to
+45
| function rawHttp(host: string, port: number, request: string): Promise<string> { | ||
| return new Promise((resolve, reject) => { | ||
| const sock = net.createConnection({ host, port }, () => sock.write(request)); | ||
| const chunks: Buffer[] = []; | ||
| sock.on('data', c => chunks.push(c)); | ||
| sock.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); | ||
| sock.on('error', reject); | ||
| // Safety timeout. | ||
| setTimeout(() => { | ||
| sock.destroy(); | ||
| resolve(Buffer.concat(chunks).toString('utf8')); | ||
| }, 3000); | ||
| }); | ||
| } |
| // Poller on main page should detect approval within 30s (checks every 1s). | ||
| await expect(page.locator('#log')).toContainText( | ||
| 'Command approved by user', | ||
| { timeout: 10_000 } |
| <div class="card"> | ||
| <h1>🔒 Command Approval Required</h1> | ||
| <p>A plugin wants to run this command:</p> | ||
| <div class="command">${command}</div> |
| <input type="hidden" name="nonce" value="${nonce}"> | ||
| <button type="submit" autofocus>Approve</button> | ||
| </form> | ||
| <p>Command: ${command}</p> |
Comment on lines
+1
to
+4
| node_modules/ | ||
| test-results/ | ||
| playwright-report/ | ||
| package-lock.json |
05b9c14 to
3f207f9
Compare
| <div class="card"> | ||
| <h1>🔒 Command Approval Required</h1> | ||
| <p>A plugin wants to run this command:</p> | ||
| <div class="command">${command}</div> |
| <input type="hidden" name="nonce" value="${nonce}"> | ||
| <button type="submit" autofocus>Approve</button> | ||
| </form> | ||
| <p>Command: ${command}</p> |
| 4466, | ||
| `GET /consent/${consentId} HTTP/1.1\r\nHost: localhost:4466\r\nSec-Fetch-Dest: document\r\nSec-Fetch-Mode: navigate\r\nConnection: close\r\n\r\n` | ||
| ); | ||
| const [headerBlock] = raw.split('\r\n\r\n', 1); |
| 4467, | ||
| `GET /consent/${consentId} HTTP/1.1\r\nHost: localhost:4467\r\nConnection: close\r\n\r\n` | ||
| ); | ||
| const [headerBlock] = raw.split('\r\n\r\n', 1); |
Comment on lines
+351
to
+359
| if (url.pathname === '/api/request-consent' && method === 'POST') { | ||
| const body = await parseBody(req); | ||
| const { command } = JSON.parse(body); | ||
| const consentId = randomId(); | ||
| const nonce = randomId(); | ||
| consents.set(consentId, { nonce, command, approved: false, used: false }); | ||
| json(res, 200, { consentId }); | ||
| return; | ||
| } |
| ("Approve Command") or the real URL (which contains the `consentId`), so the | ||
| security property holds. `window.opener === null` inside the popup is the | ||
| cross-browser-portable primitive and is asserted by | ||
| `consent-poc-adversarial.spec.ts`. |
Comment on lines
+136
to
+138
| - `tests/consent-poc.spec.ts` — 19 e2e tests covering the attack table above. | ||
| - `tests/consent-poc-adversarial.spec.ts` — 34 adversarial / negative-control | ||
| tests. |
|
|
||
| // Adversarial / negative-control tests for the consent-poc. | ||
| // | ||
| // Rationale: the positive tests in consent-poc.spec.ts could theoretically |
Comment on lines
+12
to
+18
| // Vulnerable TWIN of server.cjs used as a NEGATIVE CONTROL for the | ||
| // consent-poc tests. It exposes the same API and HTML, but | ||
| // deliberately removes the 3 server-side protections: | ||
| // | ||
| // 1. No Sec-Fetch-Dest/Mode filtering on /consent/:id | ||
| // 2. No `Cross-Origin-Opener-Policy` header on the consent page | ||
| // 3. No `Service-Worker: script` block |
Comment on lines
+32
to
+45
| function rawHttp(host: string, port: number, request: string): Promise<string> { | ||
| return new Promise((resolve, reject) => { | ||
| const sock = net.createConnection({ host, port }, () => sock.write(request)); | ||
| const chunks: Buffer[] = []; | ||
| sock.on('data', c => chunks.push(c)); | ||
| sock.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); | ||
| sock.on('error', reject); | ||
| // Safety timeout. | ||
| setTimeout(() => { | ||
| sock.destroy(); | ||
| resolve(Buffer.concat(chunks).toString('utf8')); | ||
| }, 3000); | ||
| }); | ||
| } |
… testing harness Playwright-based proof-of-concept for validating the AI Assistant consent and approval flows under both normal and adversarial conditions. Contents: - `server.cjs`: minimal Express server that simulates a legitimate MCP tool endpoint returning expected responses. - `vulnerable-server.cjs`: intentionally insecure server that omits input validation, for reproducing attack scenarios. - `consent-poc.pw-spec.ts`: happy-path Playwright suite — user sees the approval dialog, confirms, and the tool executes correctly. - `consent-poc-adversarial.pw-spec.ts`: attack scenario suite — verifies that malformed or injected tool arguments are blocked by the approval flow before reaching the server. Use these tests to verify that changes to `InlineToolApprovalManager` or the consent UI do not regress the security boundary. Signed-off-by: René Dudfield <renedudfield@microsoft.com> Co-authored-by: Ashu Ghildiyal <aghildiyal@microsoft.com> Signed-off-by: Ashu Ghildiyal <aghildiyal@microsoft.com>
3f207f9 to
afee1f5
Compare
Comment on lines
+318
to
+321
| <h1>🔒 Command Approval Required</h1> | ||
| <p>A plugin wants to run this command:</p> | ||
| <div class="command">${command}</div> | ||
| <form method="POST" action="/consent/${consentId}/approve" style="margin-top: 1.5rem;"> |
Comment on lines
+56
to
+61
| <form method="POST" action="/consent/${consentId}/approve"> | ||
| <input type="hidden" name="nonce" value="${nonce}"> | ||
| <button type="submit" autofocus>Approve</button> | ||
| </form> | ||
| <p>Command: ${command}</p> | ||
| </body></html>`; |
Comment on lines
+56
to
+58
| For older browsers without `Sec-Fetch-*` headers, the server allows the request | ||
| (headers are absent), but COOP + nonce still provide protection. The consent page | ||
| would be visible to `fetch()` but the nonce can't be replayed after use. |
Comment on lines
+32
to
+45
| function rawHttp(host: string, port: number, request: string): Promise<string> { | ||
| return new Promise((resolve, reject) => { | ||
| const sock = net.createConnection({ host, port }, () => sock.write(request)); | ||
| const chunks: Buffer[] = []; | ||
| sock.on('data', c => chunks.push(c)); | ||
| sock.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); | ||
| sock.on('error', reject); | ||
| // Safety timeout. | ||
| setTimeout(() => { | ||
| sock.destroy(); | ||
| resolve(Buffer.concat(chunks).toString('utf8')); | ||
| }, 3000); | ||
| }); | ||
| } |
Comment on lines
+187
to
+191
| // Poller on main page should detect approval within 30s (checks every 1s). | ||
| await expect(page.locator('#log')).toContainText( | ||
| 'Command approved by user', | ||
| { timeout: 10_000 } | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
docs/proposals/consent-poc/— a Playwright-based proof-of-concept adversarial test harness for validating the AI assistant's consent and approval flows, including a vulnerable test server for adversarial scenario reproduction.Assisted by Copilot.
Part of the ai-assistant design proposals.