Skip to content

Close caller-data re-injection via page snapshot + tighten fill_user_data gate (#607)#608

Merged
lmorchard merged 2 commits into
mainfrom
follow-up-caller-data-hardening
Jul 21, 2026
Merged

Close caller-data re-injection via page snapshot + tighten fill_user_data gate (#607)#608
lmorchard merged 2 commits into
mainfrom
follow-up-caller-data-hardening

Conversation

@lmorchard

Copy link
Copy Markdown
Collaborator

Follow-up to #596, closes #607. #596 hides caller data from the prompt and tool results, but two channels remained open for a value once it's placed via fill_user_data:

  1. Snapshot re-injection. The DOM element.value is serialized back into the model context by the next page snapshot (ariaSnapshot.ts). ARIA refs regenerate every snapshot, so per-ref masking is unviable — this redacts known caller-data values from the serialized snapshot string (in addPageSnapshot, before compression) and from the extract tool's page markdown, before either reaches the model. Robust to ref churn and React reconciliation. Adds collectSensitiveValues + redactSensitiveValues.
  2. Gate. fill_user_data reused assessFill, whose operational-field exemption let a secret be placed into (and, via operationalRefs, submitted from) an operational field on an untrusted host. Replaced with a dedicated assessDataFill (trusted-host-only, no operational exemption); no longer records operationalRefs.

Scope / known residuals

This narrows, but does not fully close, the re-injection surface:

  • Vision mode: the screenshot is not redacted, so a value visible on-screen re-enters via the image. This PR closes the text channel only. Exfil still requires a firewall-gated submit/navigation (TAB-1104: gate agent navigation + Tabstack URL tools through the action firewall #595).
  • Redaction is intentionally aggressive — substring match of caller values ≥6 chars — so it can over-redact common long values (e.g. a company/city name) from legitimate page text.
  • Sub-6-char values are not redacted (length threshold, to avoid false-positive over-redaction).

Testing

  • assessDataFill / collectSensitiveValues / redactSensitiveValues unit tests.
  • fill_user_data: blocked on untrusted host including an operational field (no performAction, no operationalRefs, no leak); trusted-host fill unchanged.
  • extract: page markdown redacted before the extractor sees it.
  • Full pilo-core suite green; typecheck clean; no schema drift.

🤖 Generated with Claude Code

…gate

Follow-up to #596 (tracked in #607). #596 hides caller `data` from the prompt and
the tool result, but two channels remained open:

1. Re-injection via the page snapshot. After fill_user_data places a value, the DOM
   element.value is serialized back into the model context by the next snapshot
   (ariaSnapshot.ts). ARIA refs are regenerated every snapshot, so per-ref masking
   is unviable — instead redact known caller-data values from the serialized snapshot
   string in addPageSnapshot (and from extract's page markdown), before they reach the
   model. Robust to ref churn and React reconciliation. Adds collectSensitiveValues +
   redactSensitiveValues.

2. fill_user_data reused assessFill, whose operational-field exemption let a caller
   secret be placed into (and, via operationalRefs, submitted from) an operational
   field on an untrusted host. Gate it with a dedicated assessDataFill (trusted-host
   only, no operational exemption) and stop recording operationalRefs.

Known residual: sub-6-char values are not redacted (length threshold, to avoid
false-positive redaction of common short page text).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 hardens pilo-core against re-introducing caller-provided data values into the model context after they’ve been placed into the DOM, and tightens the fill_user_data firewall gate so caller data can only be filled on caller-trusted hosts.

Changes:

  • Redacts known caller-data values from page snapshots (WebAgent.addPageSnapshot) and from extract’s page markdown before either reaches the model.
  • Introduces assessDataFill and updates fill_user_data to use it (trusted-host-only, no operational-field exemption; does not record operationalRefs).
  • Adds/updates unit tests covering the redaction helpers, the new gate, and extract redaction behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/core/src/security/actionFirewall.ts Adds assessDataFill, plus collectSensitiveValues/redactSensitiveValues helpers and a new block reason string.
packages/core/src/webAgent.ts Redacts caller-data values from serialized page snapshots before compression/model-context insertion.
packages/core/src/tools/webActionTools.ts Redacts extract markdown and tightens fill_user_data gating to trusted hosts only; stops recording operationalRefs.
packages/core/test/security/actionFirewall.test.ts Adds unit tests for assessDataFill and the sensitive-value collection/redaction helpers.
packages/core/test/tools/webActionTools.test.ts Adds tests ensuring extract prompt input is redacted and fill_user_data is blocked on untrusted operational fields.

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

Comment on lines +282 to +306
/**
* Walk an arbitrary caller-supplied `data` object and collect the leaf values worth
* redacting from page content: string and numeric leaves at or above the length
* threshold. Booleans and null are ignored. Input is a parsed JSON request body, so
* no cycle handling is needed.
*/
export function collectSensitiveValues(data: unknown): Set<string> {
const values = new Set<string>();
const visit = (node: unknown): void => {
if (node === null || node === undefined) return;
if (typeof node === "string") {
const trimmed = node.trim();
if (trimmed.length >= MIN_SENSITIVE_VALUE_LENGTH) values.add(trimmed);
} else if (typeof node === "number" || typeof node === "bigint") {
const asString = String(node);
if (asString.length >= MIN_SENSITIVE_VALUE_LENGTH) values.add(asString);
} else if (Array.isArray(node)) {
for (const item of node) visit(item);
} else if (typeof node === "object") {
for (const value of Object.values(node as Record<string, unknown>)) visit(value);
}
};
visit(data);
return values;
}
Comment thread packages/core/src/webAgent.ts Outdated
Comment on lines +1023 to +1027
if (this.data) {
currentPageSnapshot = redactSensitiveValues(
currentPageSnapshot,
collectSensitiveValues(this.data),
);
@lmorchard
lmorchard marked this pull request as draft July 20, 2026 19:19
@lmorchard

Copy link
Copy Markdown
Collaborator Author

Calling this a draft for now. Going to address copilot review and then give this at least one run through evals

- collectSensitiveValues: guard against cyclic `data` (typed `any`, may be built
  programmatically) with a WeakSet, so redaction fails safe instead of
  stack-overflowing the agent loop. Adds a cyclic-input test.
- Compute the sensitive-value set once (WebAgent.sensitiveDataValues, set when
  `data` is assigned, cleared in clearInternalState) instead of re-walking `data`
  on every snapshot. Thread it into the tool context so `extract` reuses it too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmorchard

Copy link
Copy Markdown
Collaborator Author

Addressed both Copilot comments in 7db5b91:

  • Cyclic data (collectSensitiveValues): added a WeakSet cycle guard so a programmatically-built data with a reference cycle fails safe instead of stack-overflowing the redaction/agent loop. Added a cyclic-input test.
  • Repeated deep walk (addPageSnapshot): the sensitive-value set is now computed once (WebAgent.sensitiveDataValues, set when data is assigned, cleared in clearInternalState) rather than re-walked per snapshot, and threaded into the tool context so extract reuses it too.

Full pilo-core suite green (944), typecheck + prettier clean, no schema drift.

@lmorchard
lmorchard marked this pull request as ready for review July 21, 2026 16:38
@lmorchard

Copy link
Copy Markdown
Collaborator Author

80% partial eval pass, nothing obvious broken

image

@lmorchard
lmorchard merged commit 51d73f6 into main Jul 21, 2026
17 checks passed
@lmorchard
lmorchard deleted the follow-up-caller-data-hardening branch July 21, 2026 16:39
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.

Follow-up (after #596): close caller-data re-injection via page snapshot + tighten data-fill gate

3 participants