Skip to content

TAB-976 Harden form fills against prompt context exfiltration#478

Open
srbiv wants to merge 7 commits into
mainfrom
stafford/tab-976-harden-pilo-against-web-content-prompt-injection
Open

TAB-976 Harden form fills against prompt context exfiltration#478
srbiv wants to merge 7 commits into
mainfrom
stafford/tab-976-harden-pilo-against-web-content-prompt-injection

Conversation

@srbiv
Copy link
Copy Markdown
Collaborator

@srbiv srbiv commented May 26, 2026

Summary

  • Add a structural action firewall for form fills and submissions using DOM field metadata and ref provenance, not payload text matching
  • Allow agent-filled operational controls such as search/date/range inputs, while blocking unapproved freeform or sensitive fields before browser execution
  • Track agent-filled, operational, and user-approved refs so submit actions are blocked when a form contains unauthorized agent-filled data
  • Avoid echoing blocked payloads in recoverable tool results
  • Add unit/tool/agent regression coverage for unauthorized fills, submit gating, provenance lifetime, and missing provenance state

Test Plan

  • pnpm run format
  • pnpm run format:check
  • pnpm run typecheck
  • pnpm -r run test
  • gitleaks protect -v
  • pnpm exec tsx scripts/verify-prompt-injection-firewall.ts (local uncommitted verifier; passes on this branch and is designed to fail on main)

Copy link
Copy Markdown
Collaborator

@lmorchard lmorchard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is okay to land - though, it does break some form-filling stuff I was doing for automated survey testing. Not sure if we have any real customers relying on something similar.

The solution for the survey testing would be to use a separate companion agent to provide the survey answers in interactive mode. But that's probably a better design anyway, since that agent won't necessarily be exposed to prompt injection from the page itself as easily.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a structural “action firewall” to harden browser automation against prompt-context exfiltration by gating agent-driven form fills and form submissions using DOM-derived field metadata and provenance tracking (rather than payload text matching).

Changes:

  • Add FieldMetadata / FormSubmissionContext browser APIs and implement them in Playwright + Extension browsers.
  • Enforce a security policy in webActionTools to block unauthorized agent fills and to gate submit-like actions when a form contains unauthorized agent-filled refs.
  • Add unit/regression tests for firewall behavior, provenance requirements, and snapshot/provenance lifetime interactions.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/extension/src/background/ExtensionBrowser.ts Adds DOM introspection helpers for field metadata + form submission context.
packages/core/src/browser/ariaBrowser.ts Extends the AriaBrowser contract with metadata/submission context types and methods.
packages/core/src/browser/playwrightBrowser.ts Implements the new browser APIs using Playwright locator.evaluate.
packages/core/src/security/actionFirewall.ts Adds fill/submission assessment logic and constants.
packages/core/src/tools/webActionTools.ts Integrates firewall checks + provenance tracking into tools (fill/click/enter).
packages/core/src/tools/interactiveTools.ts Simplifies ApprovedRefs implementation (now a Set).
packages/core/src/webAgent.ts Wires provenance sets into tool context; adjusts snapshot refresh policy for fill.
packages/core/src/core.ts Re-exports the new browser types from the public core entrypoint.
packages/core/test/security/actionFirewall.test.ts Adds unit coverage for the firewall’s allow/block decisions.
packages/core/test/tools/webActionTools.test.ts Adds regression tests for blocked fills, submit gating, and provenance requirements.
packages/core/test/webAgent.test.ts Adds regression coverage for snapshot stability after fill.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (error instanceof BrowserException) {
return failedActionResult(action, error.message, context, ref);
}
throw error;
if (error instanceof BrowserException) {
return failedActionResult(PageAction.Fill, error.message, context, ref);
}
throw error;
Comment on lines +899 to +971
return locator.evaluate(
(element, { submitterRef, trigger }): FormSubmissionContext | null => {
const el = element as HTMLElement;
if (!canSubmitForm(el, trigger)) return null;

const form = getSubmissionForm(el);
if (!form) return null;

const fields = Array.from(form.elements)
.filter(
(field): field is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement =>
field instanceof HTMLInputElement ||
field instanceof HTMLTextAreaElement ||
field instanceof HTMLSelectElement,
)
.filter((field) => !field.disabled)
.map((field) => ({
ref: field.getAttribute("data-pilo-ref"),
name: field.name || null,
tagName: field.tagName.toLowerCase(),
inputType: field instanceof HTMLInputElement ? field.type.toLowerCase() : null,
autocomplete: "autocomplete" in field ? field.autocomplete || null : null,
}));

return {
submitterRef,
formId: form.id || null,
actionUrl: form.action || null,
method: form.method?.toLowerCase() || null,
fields,
};

function getSubmissionForm(node: HTMLElement): HTMLFormElement | null {
if (
node instanceof HTMLButtonElement ||
node instanceof HTMLInputElement ||
node instanceof HTMLTextAreaElement ||
node instanceof HTMLSelectElement
) {
return node.form;
}
return node.closest("form");
}

function canSubmitForm(node: HTMLElement, submitTrigger: FormSubmissionTrigger): boolean {
if (submitTrigger === "click") {
if (node instanceof HTMLButtonElement) {
return node.type === "submit";
}
if (node instanceof HTMLInputElement) {
return node.type === "submit" || node.type === "image";
}
return false;
}

if (node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)
return false;
if (!(node instanceof HTMLInputElement)) return false;
return ![
"button",
"checkbox",
"color",
"file",
"hidden",
"radio",
"range",
"reset",
"submit",
].includes(node.type);
}
},
{ submitterRef: ref, trigger },
);
Comment on lines +316 to +321
func: (elementRef: string) => {
const element = document.querySelector(`[data-pilo-ref="${elementRef}"]`);
if (!(element instanceof HTMLElement)) {
throw new Error(`Element with ref ${elementRef} not found in DOM`);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants