Skip to content

feat(console): a rejection keeps its code across the bridge - #171

Merged
stormer78 merged 1 commit into
mainfrom
feat/relay-error-code
Sep 7, 2026
Merged

feat(console): a rejection keeps its code across the bridge#171
stormer78 merged 1 commit into
mainfrom
feat/relay-error-code

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Why

packages/core throws a typed VtaClientError carrying a stable code and structured details. Every offscreen/background message handler collapsed it to a string:

.catch((e: unknown) => sendResponse({ ok: false, error: e instanceof Error ? e.message : String(e) }))

and interpretOutcome then re-wrapped that string in a fresh Error. By the time a console pane saw a failure, the code and the details were gone and only prose remained.

That directly contradicts R3.7match errors on stable machine-readable codes, never on strings. With prose as the only signal, a pane that wants to behave differently for one particular refusal has exactly one option left: matching the message text, which the rule forbids for the obvious reason that the agent may reword a message whenever it likes and may not change a code.

The case that forced it: persona/profile/delete refuses while personas are still bound to the profile, with an extended code and a details.personaDids naming them. The console could render neither the reason nor the list.

What a pane can now do that it could not

Catch RelayTaskError and branch:

catch (e) {
  if (e instanceof RelayTaskError && e.code === "persona/profile/delete:profileInUse") {
    const dids = (e.details as { personaDids?: string[] }).personaDids ?? [];
    // …offer to unbind those personas, naming them
  }
  // …otherwise render e.message, exactly as today
}

It can also tell "the VTA refused" from "the VTA never answered" (e.client.timeout) without reading a sentence.

What changed

  • bridge-protocol.ts — new RelayTaskFailure: an optional code and details beside the human error string, which stays REQUIRED. Most failures have no code worth switching on and prose is all a pane has. RuntimeManagerTaskResponse and the new OffscreenRequestTaskResponse use it.
  • relay-failure.ts (new) — the one place a rejection becomes a reply. Three decisions live there, each documented against the failure it prevents:
    • it prefers the agent's own wire code over the VtaErrorCode the client coerced it to. coerceTrustTaskCode funnels every extended code it does not recognise into e.p.msg.bad_request, and its own doc comment says a caller that needs the meaning must read the raw code off details. This is that caller.
    • VtaClientError.details arrives in two shapes — the whole trust-task-error payload (parseTrustTaskReply) or the server's error.details directly (errorFromBody). Both are handled; reading one and assuming the other yields undefined for everything, which reads as "the agent sent no code" rather than as a bug.
    • details is round-tripped through JSON, because chrome.runtime.sendMessage serializes: an Error instance would arrive as {}, which is worse than absent — if (details) takes the branch and finds nothing. An unserializable or empty value is dropped, and dropping it never costs the code or the sentence.
  • offscreen.ts — the OFFSCREEN_REQUEST_TASK branch uses relayFailure. Only that branch: the others answer wallet UI that renders prose and nothing else.
  • carrier.tsinterpretOutcome throws the new RelayTaskError instead of a bare Error, following the ConsentRequiredError precedent in that file. Message text is byte-identical, so anything rendering .message is unaffected. carrier.ts stays free of relative imports.
  • background.tshandleManagerTask passes the reply through whole; the dispatch catch for that branch uses relayFailure too.

What deliberately did NOT change

  • ConsentRequiredError. Untouched, and a test asserts a consentRequired outcome still throws it and not RelayTaskError — reclassifying the ceremony as a failure would render it as a red string at the moment the human was supposed to act.
  • The page-facing relay (RUNTIME_REQUEST_TASK / content.ts). One offscreen handler answers both relays, so the narrowing is a deliberate act: handleRequestTask now rebuilds { ok: false, error: res.error } rather than casting the reply. A page proposed a task and is entitled to know it was refused, not to read the agent's internal account of why — a details object naming other personas, other contexts or an ACL's contents is a disclosure, and it would reach any site that called requestTask and caught the error. Written as a reconstruction because a cast narrows the type and copies the object whole.
  • No compatibility fold. Nothing is deployed; the shapes cut over together.
  • No pane refactored to consume it. That is the follow-up. The tests carry the proof that the plumbing is real.

Tests

packages/extension/tests/relay-error-code.test.mts — 14 tests. Every negative assertion is paired with a positive one, because a test that only checks something is absent passes just as happily against an implementation that returns nothing at all.

  • The refusal is built by running core's own parseTrustTaskReply over a real trust-task-error/0.3 document, not by hand-rolling a VtaClientError. A stub built to match what relayFailure reads would agree with it by construction and prove nothing. One test asserts the premise — that core nests the agent's code inside details and leaves err.code as the coerced bucket — so the rest cannot pass on the fallback path.
  • Code and details intact at the caller, through a structuredClone.
  • A rejection with neither still produces a readable error; a non-Error throw too.
  • A client-side e.client.timeout keeps its own code; an errorFromBody refusal keeps details from the other depth.
  • An Error (and a cyclic object) handed in as details is dropped, and dropping it costs neither the code nor the sentence.
  • ConsentRequiredError unchanged.
  • Two source-read assertions, in the register of manager-surface.test.mts, that the page reply shape stays prose-only and that the console's carries RelayTaskFailure.

Verified

  • npm run lint (tsc -b), npm run build, npm test — all four workspaces green: tsp-js 51, core 514, extension 228, reviewer-demo 9.
  • Every Assert* step from .github/workflows/ci.yml run locally against the real dist/: single MV3 worker bundle with no dynamic import(); 17 admin URIs confined to manager.js; 3 seeds URIs absent everywhere; 10 holder-scoped persona URIs confined to manager.js and still present there; console a single chunk; Web Store zip uploadable (no key, version matches, no static content_scripts, no cookies, no chrome.cookies).

`packages/core` throws a typed `VtaClientError` carrying a stable `code`
and structured `details`, and every message handler on the extension's
bridge collapsed it to `e.message`. `interpretOutcome` then re-wrapped
that string in a fresh `Error`, so by the time a console pane saw a
failure only prose remained.

That leaves a pane one way to act on a particular refusal — matching on
the message text — which is exactly what R3.7 forbids: the agent may
reword a message whenever it likes and may not change a code. The case
that forced it is `persona/profile/delete`, refused while personas are
still bound with an extended code and a `details.personaDids` naming
them; the console could render neither.

The console relay (`RUNTIME_MANAGER_TASK`) now carries both end to end:

- `RelayTaskFailure` in `bridge-protocol.ts` adds an optional `code` and
  `details` beside the human string, which stays REQUIRED — most
  failures have no code and prose is all a pane has.
- `relay-failure.ts` is the one place a rejection becomes a reply. It
  prefers the agent's own wire code over the `VtaErrorCode` the client
  coerced it to (that coercion buckets every unrecognised extended code
  into `bad_request`), reads both shapes `VtaClientError.details`
  arrives in, and round-trips the value through JSON so what a pane
  receives survives the message channel.
- `carrier.ts` throws `RelayTaskError` — a typed throw beside the
  existing `ConsentRequiredError`, with the same message text as before.
  `ConsentRequiredError` is untouched.

The page-facing relay is deliberately not widened. One offscreen handler
answers both, so `handleRequestTask` rebuilds the failure from the error
string alone: a site is entitled to know its task was refused, not to
read the agent's account of why.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 4925edd into main Sep 7, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/relay-error-code branch September 7, 2026 09:52
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

1 AI-confirmed issue, 4 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #171

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/relay-error-codemain
Validated 2026-09-07
Scan ID a20a2738
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 6 · findings: 7

Module Files scanned Findings
packages/extension 6 7

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 1 4

⚠️ 4 finding(s) need human review. These could not be conclusively confirmed or dismissed automatically (insufficient evidence). They are not dismissed — a developer / security team member must read and decide.


🔒 Security Issues

Confirmed Vulnerabilities (1)

🟡 jsonSafe() silently discards unserializable or empty 'details' payloads with no logging, weakening forensic traceability of anomalous agent responses

Field Detail
Severity MEDIUM
Location packages/extension/src/relay-failure.ts:95
Finding ID github_pr-330e76f1102b
CWE CWE-778, CWE-223
OWASP A09:2021 - Security Logging and Monitoring Failures
MITRE ATT&CK T1070 - Indicator Removal (analogous, non-malicious in current code but a facilitator)
CAPEC CAPEC-268
DREAD 2.4
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — No CVSS applies since this is a first-party code observability defect, not an externally exploitable flaw. The code evidence confirms silent data loss on serialization failure, but the impact is purely diagnostic/forensic degradation (loss of error detail context), not confidentiality, integrity, or availability compromise of application functionality. There is no attacker-controlled exploitation path — the trigger is a malformed or non-serializable error payload from the agent/server, not an attacker-crafted request achieving privilege escalation or data access. This correctly stays at low.
  • Composite score: 5.1
  • Environment: production

📝 Description:

An operator investigating a refused administration task in the management console would see a bare error message with no details, unable to tell whether the agent genuinely provided no structured context or whether an anomalous/malformed details object was silently dropped — reducing incident-response and debugging visibility, and potentially masking a misbehaving or adversarial VTA/agent server.

🧪 Proof of Concept:

Both the catch branch and the empty-object branch return undefined with no side-channel record of the fact that a non-trivial input was discarded, meaning a caller downstream (relayFailure, then the console UI) cannot distinguish 'agent sent no details' from 'agent sent details we couldn't represent'.

function jsonSafe(value: unknown): unknown | undefined {
  if (value === undefined || value === null) return undefined;
  let round: unknown;
  try {
    round = JSON.parse(JSON.stringify(value)) as unknown;
  } catch {
    return undefined;
  }
  if (round === undefined || round === null) return undefined;
  if (typeof round === "object" && !Array.isArray(round) && Object.keys(round).length === 0) {
    return undefined;
  }
  return round;
}

Vulnerable lines: 90, 112

🔎 Evidence: packages/extension/src/relay-failure.ts:95

function jsonSafe(value: unknown): unknown | undefined {
  if (value === undefined || value === null) return undefined;
  let round: unknown;
  try {
    round = JSON.parse(JSON.stringify(value)) as unknown;
  } catch {
    return undefined;
  }

💥 Impact:

An operator investigating a refused administration task in the management console would see a bare error message with no details, unable to tell whether the agent genuinely provided no structured context or whether an anomalous/malformed details object was silently dropped — reducing incident-response and debugging visibility, and potentially masking a misbehaving or adversarial VTA/agent server.

Confidentiality: None. · Integrity: None directly; indirectly weakens the operator's ability to detect integrity issues at the agent. · Availability: None.

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: Agent/VTA server error response → VtaClientError.details → relayFailure() in relay-failure.ts → jsonSafe(details) → silent drop (no log) → console receives RelayTaskFailure without details

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Adding an optional diagnostic callback (invoked only for extension-internal debug logging, never surfaced to the console UI or a web page) preserves the existing security property (no misleading empty-object disclosure) while restoring forensic visibility into anomalous agent behavior.

Vulnerable code:

function jsonSafe(value: unknown): unknown | undefined {
  if (value === undefined || value === null) return undefined;
  let round: unknown;
  try {
    round = JSON.parse(JSON.stringify(value)) as unknown;
  } catch {
    return undefined;
  }
  ...
}

Secure code:

function jsonSafe(value: unknown, diag?: (reason: string) => void): unknown | undefined {
  if (value === undefined || value === null) return undefined;
  let round: unknown;
  try {
    round = JSON.parse(JSON.stringify(value)) as unknown;
  } catch {
    diag?.("details discarded: not JSON-serializable");
    return undefined;
  }
  if (round === undefined || round === null) return undefined;
  if (typeof round === "object" && !Array.isArray(round) && Object.keys(round).length === 0) {
    diag?.("details discarded: serialized to empty object (e.g. Error instance)");
    return undefined;
  }
  return round;
}

Additional recommendations:

  • Add a metrics counter incremented on each drop event to detect patterns over time (e.g. a compromised VTA server repeatedly sending malformed details).
  • Consider capturing a truncated/redacted string summary (e.g. Object.prototype.toString.call(value)) for debug logs when full serialization fails.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: relay-failure.ts's jsonSafe() function: try { round = JSON.parse(JSON.stringify(value)) as unknown; } catch { return undefined; } and further if (typeof round === "object" && !Array.isArray(round) && Object.keys(round).length === 0) { return undefined; } — both branches silently discard the value with no logging, telemetry, or error recording of any kind, confirmed by reading the full function body. EVIDENCE NOT FOUND: No logging call (console.warn, telemetry hook, etc.) exists anywhere in this file or the discard paths. CHANGED VS PRE-EXISTING: relay-failure.ts is a new file introduced by this PR's relay-error-code feature (confirmed by its extensive PR-specific doc comments referencing R3.7 and this exact feature); CHANGED. VERDICT JUSTIFICATION: The code is fully visible and confirms the exact behavior described — a low-severity forensic/observability gap, not a security vulnerability but a legitimate, evidenced logging gap; validated as accurately describing the code's actual (intentional per comments) behavior.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

⚠️ Must-Review-By-Human (4)

Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.

🟡 Structured details from agent passed to console without sanitization before rendering

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/carrier.ts:95
Finding ID github_pr-8aaf6d263d2b
CWE CWE-79
OWASP A03:2021 - Injection
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE not specified but consistent with CWE-79 (XSS via unsanitized data) or CWE-1021 (improper output neutralization). No confirmed rendering sink was shown in the evidence — the vulnerable file only demonstrates that `details` is untyped and unsanitized at assignment time, not that it's rendered unsafely. Exploitation requires attacker control of an agent's relay payload (elevated precondition vs anonymous user). No CVSS, no exploit maturity, no EPSS. Scanner's own confidence is only 55%. This maps to Medium: real design flaw, plausible but unconfirmed sink, requires a non-trivial precondition (compromised/malicious agent).
  • Composite score: 5.1
  • Environment: production

📝 Description:

RelayTaskError.details carries unvalidated wire data from the agent (e.g. personaDids, ACL contents) directly into the console UI layer. The code comments acknowledge this is 'unvalidated wire data' but no sanitization/encoding is enforced at this boundary before a pane might render it (e.g., into HTML), creating a potential XSS or injection vector if a consuming pane does not defensively encode it.

🌱 Root Cause: details is typed as unknown and passed through verbatim from agent-controlled JSON without any content validation, escaping, or schema enforcement before reaching UI-rendering code paths.

🔎 Evidence: packages/extension/src/manager/carrier.ts:95

readonly details?: unknown;
  /** The task type the operator was attempting. */
  readonly taskType: string;

  constructor(taskType: string, message: string, failure: RelayFailureFields) {
    super(message);
    this.name = "RelayTaskError";
    this.taskType = taskType;
    if (failure.code !== undefined) this.code = failure.code;
    if (failure.details !== undefined) this.details = failure.details;
  }

🎯 Attack Scenario:

A malicious or compromised Trust-Task agent returns a details payload containing script-like strings (e.g. in personaDids or other free-text fields). If any console pane renders details fields via innerHTML or unescaped templating, this results in stored/reflected XSS in the operator console.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: carrier.ts defines RelayTaskError with readonly details?: unknown; and comments explicitly state 'it is unvalidated wire data and every member has to be checked before it is used'. No console pane rendering code (e.g. innerHTML usage) was provided in source_files to confirm actual unsanitized rendering. EVIDENCE NOT FOUND: No pane/UI component that reads details and inserts it into the DOM via innerHTML or similar unsafe sink was found in the provided files (ui.tsx shows only Did/Pill/Button/Panel/Note components using React JSX text interpolation, which auto-escapes). CHANGED VS PRE-EXISTING: carrier.ts is directly quoted in the finding evidence and is part of this PR's relay-error-code feature (RelayTaskError is a new class per the PR description), so this is CHANGED code. VERDICT JUSTIFICATION: The details field is typed unknown and documented as requiring validation before use, but no actual rendering/sink code was found reachable from this file to confirm a real XSS or unsafe use — cannot confirm exploitability, so must_review rather than validated or dismissed.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🟡 Agent-controlled error code/details relayed to console enables logic bypass if pane trusts code equality checks without validating details shape

Field Detail
Severity MEDIUM
Location packages/extension/src/bridge-protocol.ts:1820
Finding ID github_pr-d15cae618742
CWE CWE-20
OWASP A08:2021-Software and Data Integrity Failures
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • The evidence is a type definition (interface field typed 'unknown') rather than a proven vulnerable sink. The attack requires a malicious/compromised agent — a non-trivial precondition — and the scanner itself only asserts a logic-bypass risk with 40% confidence and no confirmed reachability to a specific vulnerable consumer. This aligns with medium: a plausible but unconfirmed logic weakness, not a clear high-impact injection/RCE/auth-bypass with reachable exploit path.
  • Composite score: 4.9
  • Environment: production

📝 Description:

The bridge protocol trusts the remote agent's self-reported code and details fields verbatim and forwards them to the privileged console surface for branching logic (R3.7), without any allowlist or schema validation of the code namespace or the details shape at the trust boundary.

🌱 Root Cause: No validation is performed on the agent-supplied code/details before they are used for programmatic branching in consuming panes; the comment explicitly states 'unvalidated wire data' and defers all checking to the caller.

🔎 Evidence: packages/extension/src/bridge-protocol.ts:1820

code?: string;
  /**
   * The task-specific structured context the agent sent with its refusal —
   * `TrustTaskErrorPayload.details`, e.g. `{ personaDids: [...] }`.
   */
  details?: unknown;

🎯 Attack Scenario:

A malicious or compromised VTA agent could send crafted code values that collide with expected extended codes (e.g., a fake persona/profile/delete:profileInUse) to trick the console into taking an unintended workflow branch, or embed oversized/malformed details to affect downstream logic that assumes well-formed shapes.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 40%
  • AI Validation Evidence: EVIDENCE FOUND: bridge-protocol.ts RelayTaskFailure type includes code?: string and details?: unknown fields, per the quoted snippet, forwarded from the agent without schema validation shown in relay-failure.ts's relayFailure() function, which only does jsonSafe() (JSON round-trip) not shape/allowlist validation of code. EVIDENCE NOT FOUND: No console pane code was provided showing branching logic that trusts code equality without validating details shape, so the actual 'logic bypass' impact cannot be confirmed. CHANGED VS PRE-EXISTING: bridge-protocol.ts's RelayTaskFailure type is part of this PR's relay-error-code feature; CHANGED. VERDICT JUSTIFICATION: The lack of runtime validation is real but the described exploit (logic bypass in pane code trusting code without validating details) is not demonstrated in any provided pane code — insufficient evidence to confirm exploitability, must_review.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🟡 Single manual code path is the only enforcement of the page/console information-disclosure boundary (no compiler- or runtime-enforced separation)

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:1855
Finding ID github_pr-fd32ebf40259
CWE CWE-668, CWE-1173
OWASP A01:2021 - Broken Access Control
MITRE ATT&CK T1552 - Unsecured Credentials (analogous: sensitive-context disclosure)
CAPEC CAPEC-118, CAPEC-224
DREAD 3.2
Reachability ⚪ Not reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • The scanner confirms the vulnerable disclosure is NOT currently reachable (is_reachable=false) because the mitigation is present and correct at background.ts:1855-1858; exploit_maturity is theoretical and there is no public exploit. This is a structural/architectural risk (single point of manual enforcement, no regression test or type-level guarantee) rather than an active info-disclosure flaw. Medium accurately reflects a real but currently-mitigated risk with a plausible, low-effort regression path; it does not meet high/critical criteria (no confirmed reachable disclosure today, no exploit evidence) and is not low because the blast radius on regression (public, unauthenticated web pages) and the ease of accidental regression are non-trivial.
  • Composite score: 5
  • Environment: production

📝 Description:

If this control is ever removed or bypassed in a future change, any website that calls the extension's exposed requestTask() bridge API would receive, on a refused Trust-Task, the agent's internal refusal code and structured details — potentially including personaDids naming other personas/profiles bound to the wallet, or other ACL-derived context — allowing a malicious site to enumerate a victim's wallet-internal state that it has no legitimate right to see.

🧪 Proof of Concept:

The explicit reconstruction on the last two lines is the sole control preventing res.code and res.details (populated upstream by relayFailure()) from reaching the page. Nothing in the type system prevents a future edit from removing this reconstruction; TypeScript casts (as X) do not strip runtime object members, so a regression to a cast-based return would silently restore the leak.

  const res = (await chrome.runtime.sendMessage({
    target: OFFSCREEN_TARGET,
    type: OFFSCREEN_REQUEST_TASK,
    vtaDid: active.conn.vtaDid,
    restBaseUrl: active.conn.restBaseUrl,
    origin: req.origin,
    params: req.params,
  })) as OffscreenRequestTaskResponse;

  // ... consent-replay bookkeeping ...

  if (res.ok) return res;
  return { ok: false, error: res.error };
}

Vulnerable lines: 1840, 1860

🔎 Evidence: packages/extension/src/background.ts:1855

if (res.ok) return res;
  return { ok: false, error: res.error };

💥 Impact:

If this control is ever removed or bypassed in a future change, any website that calls the extension's exposed requestTask() bridge API would receive, on a refused Trust-Task, the agent's internal refusal code and structured details — potentially including personaDids naming other personas/profiles bound to the wallet, or other ACL-derived context — allowing a malicious site to enumerate a victim's wallet-internal state that it has no legitimate right to see.

Confidentiality: High if the guard is removed — persona/ACL identifiers and refusal context would leak cross-origin. · Integrity: None directly. · Availability: None directly.

🧭 Reachability:

  • Network exposure: public
  • Auth barrier: none
  • Attack path: EP-001 (RUNTIME_REQUEST_TASK from web page) → background.ts handleRequestTask() → OFFSCREEN_REQUEST_TASK (EP-003) → offscreen.ts doRequestTask()/relayFailure() → [CURRENTLY MITIGATED: explicit reconstruction strips code/details at background.ts ~line 1855-1858] → RuntimeRequestTaskResponse to page

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact low
Public exploit None known
Environment unknown

Attack scenario: A malicious web page could receive agent-internal refusal details (persona IDs, ACL context) if a future code change removes the current explicit stripping logic in handleRequestTask; today this is correctly mitigated.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Extracting the stripping logic into a single named, exhaustively-typed utility function (rather than an inline reconstruction repeated wherever needed) reduces the chance that a future edit at any call site accidentally reverts to a lossy cast. Pairing this with a custom ESLint rule that flags as RuntimeRequestTaskResponse / as RuntimeManagerTaskResponse applied to an OffscreenRequestTaskResponse-typed value, plus an explicit unit test asserting the page path never contains code/details even when the mocked offscreen response is a full RelayTaskFailure, converts this manual invariant into an enforced one.

Vulnerable code:

// Risky pattern the codebase explicitly warns against and must never reintroduce:
return res as RuntimeRequestTaskResponse; // copies res whole, including code/details

Secure code:

// Prefer a single shared, exhaustively-typed stripping utility used at every
// page-facing boundary, so the safety property is enforced once and reused,
// not re-derived by hand at each call site:
function toPageSafeResponse(res: OffscreenRequestTaskResponse): RuntimeRequestTaskResponse {
  if (res.ok) return { ok: true, result: res.result };
  return { ok: false, error: res.error };
}
// usage:
return toPageSafeResponse(res);

Additional recommendations:

  • Add the regression test suggested in relay-error-code.test.mts style: feed handleRequestTask a mock OFFSCREEN_REQUEST_TASK response containing code+details and assert the returned page response has neither key.
  • Add a custom ESLint rule banning as RuntimeRequestTaskResponse / as RuntimeManagerTaskResponse casts on OffscreenRequestTaskResponse-typed expressions.
  • Consider a branded/opaque return type for page-facing handlers that structurally cannot carry extra members, enforced by a shared builder function.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: The evidence snippet if (res.ok) return res; return { ok: false, error: res.error }; in background.ts shows an explicit narrowing pattern that strips code/details before returning to the page-facing caller — this is a mitigation, not a vulnerability, matching the pattern also cited in finding github_pr-e1b5c09df3d0. EVIDENCE NOT FOUND: offscreen.ts (referenced as the file with the 'single manual code path') was not included in source_files, so the actual handleRequestTask/handleManagerTask dispatch logic and any structural/compile-time enforcement (or lack thereof) could not be directly verified. CHANGED VS PRE-EXISTING: background.ts line 1855 is explicitly part of this PR's relay-error-code changes (matches evidence quoted in multiple findings tied to this PR); CHANGED. VERDICT JUSTIFICATION: This finding is speculative about future misuse of a shared code path rather than a demonstrated current vulnerability; the current code actually strips sensitive fields correctly, so while the architectural concern (single point of enforcement) may be valid design feedback, it is not a confirmed exploitable finding in the current code — must_review for human judgment on the architectural risk.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🟡 chrome.runtime.sendMessage response cast without runtime schema validation at background/offscreen trust boundary

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:1848
Finding ID github_pr-378290c3a07c
CWE CWE-20, CWE-704
OWASP A04:2021 - Insecure Design
CAPEC CAPEC-153
DREAD 2.4
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — No CVSS score exists (this is a first-party code-quality finding). Exploitability is scanner-rated 'low' with 'theoretical' maturity — the attack path is same-extension (background→offscreen), not attacker-controlled from a web page. Business impact is capped at 'transient unresponsiveness of a single task call' with no data exposure or corruption. Environment is unknown but even assuming production, the lack of any attacker-reachable input path and the narrow, self-limiting blast radius keep this at low severity.
  • Composite score: 4.6
  • Environment: production

📝 Description:

An unexpected or malformed offscreen response could cause an unhandled exception during response processing for a single requestTask() or manager-task call, resulting in that specific call hanging without a reply (the page or console's promise would never resolve until any surrounding timeout), degrading reliability rather than confidentiality/integrity.

🧪 Proof of Concept:

The cast provides only compile-time type information; nothing verifies at runtime that the message-channel result actually has an ok boolean and the expected nested shape. res.result?.kind uses optional chaining defensively, but other accesses (and the initial res.ok check itself) assume the cast is truthful. If the offscreen side ever returns something structurally different (e.g. due to an uncaught exception inside the offscreen listener before sendResponse is called, causing chrome to deliver undefined), res.ok on undefined would throw, potentially before sendResponse is invoked in the outer .then/.catch chain for that message.

  const res = (await chrome.runtime.sendMessage({
    target: OFFSCREEN_TARGET,
    type: OFFSCREEN_REQUEST_TASK,
    vtaDid: active.conn.vtaDid,
    restBaseUrl: active.conn.restBaseUrl,
    origin: req.origin,
    params: req.params,
  })) as OffscreenRequestTaskResponse;

  // A consent refusal is the only thing that arms a replay, and it carries the
  // VTA's own salted digest — the same value its `task-consent/granted` notice
  if (res.ok && res.result?.kind === "consentRequired") {
    const digest = res.result.payloadDigest;

Vulnerable lines: 1838, 1852

🔎 Evidence: packages/extension/src/background.ts:1848

  })) as OffscreenRequestTaskResponse;

💥 Impact:

An unexpected or malformed offscreen response could cause an unhandled exception during response processing for a single requestTask() or manager-task call, resulting in that specific call hanging without a reply (the page or console's promise would never resolve until any surrounding timeout), degrading reliability rather than confidentiality/integrity.

Confidentiality: None. · Integrity: None. · Availability: Low — potential for a specific call to hang without response.

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001/EP-002 → handleRequestTask/handleManagerTask → chrome.runtime.sendMessage(OFFSCREEN_REQUEST_TASK) → unchecked as OffscreenRequestTaskResponse cast → field access (res.result.payloadDigest)

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact low
Public exploit None known
Environment unknown

Attack scenario: An internal, same-extension malformed message (not shown as attacker-reachable from a web page) could cause an unhandled exception in response processing, stalling a single task call.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Validating the shape at runtime with a schema library converts a silent/unchecked assumption into an explicit, handled failure mode, guaranteeing sendResponse is always called with a well-formed reply even when the offscreen document misbehaves.

Vulnerable code:

const res = (await chrome.runtime.sendMessage({ ... })) as OffscreenRequestTaskResponse;
if (res.ok && res.result?.kind === "consentRequired") { ... }

Secure code:

import { z } from "zod";

const OffscreenResponseSchema = z.union([
  z.object({ ok: z.literal(true), result: z.record(z.unknown()) }),
  z.object({ ok: z.literal(false), error: z.string(), code: z.string().optional(), details: z.unknown().optional() }),
]);

const raw = await chrome.runtime.sendMessage({ ... });
const parsed = OffscreenResponseSchema.safeParse(raw);
if (!parsed.success) {
  return { ok: false, error: "internal relay returned an unexpected response shape" };
}
const res = parsed.data;
if (res.ok && (res.result as any)?.kind === "consentRequired") { ... }

Additional recommendations:

  • Apply the same validation pattern to every chrome.runtime.sendMessage boundary crossing in this extension, not just OFFSCREEN_REQUEST_TASK.
  • Add fuzz tests that feed malformed offscreen responses into handleRequestTask/handleManagerTask and assert sendResponse is always eventually called.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: The quoted snippet })) as OffscreenRequestTaskResponse; in background.ts shows a TypeScript type cast without visible runtime schema validation (no zod/io-ts usage found anywhere in provided source_files). EVIDENCE NOT FOUND: The full handleRequestTask function body and any try/catch wrapping around downstream property access (e.g. res.result.payloadDigest) was not fully provided, so the actual crash/DoS impact from a malformed response cannot be confirmed. CHANGED VS PRE-EXISTING: This cast is part of background.ts's relay-error-code changes referenced across multiple findings in this PR; CHANGED. VERDICT JUSTIFICATION: A real code pattern (unchecked cast) is confirmed, but the described DoS/crash impact is speculative and unverified without the full function body and offscreen.ts response-shape guarantees — must_review.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.


Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.

Complementary: 🛡️ **Threat Model & Affect Analysis**
Details

🛡️ Threat Model & Affect Analysis — PR #171

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/relay-error-codemain
Generated 2026-09-07

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

Introduces a machine-readable error-code/details channel (RelayTaskFailure) for refusals crossing the background<->offscreen<->manager-console bridge, replacing lossy e.message string collapse with a typed relay (relayFailure/RelayTaskError) so the operator console can branch on stable codes instead of parsing prose. Deliberately preserves the existing prose-only failure shape for the page-facing relay (handleRequestTask) to avoid disclosing agent-internal refusal context (e.g. persona/ACL details) to untrusted web origins.

Diff: +245 / -32 lines
Types: security, feature, refactor

📁 File Classifications

packages/extension/src/background.ts

  • Type: security

packages/extension/src/bridge-protocol.ts

  • Type: security

packages/extension/src/manager/carrier.ts

  • Type: security

packages/extension/src/offscreen.ts

  • Type: security

packages/extension/src/relay-failure.ts

  • Type: security

packages/extension/tests/relay-error-code.test.mts

  • Type: test

🛡️ STRIDE Threat Model

Identified Threats (11)

🟠 STRIDE-1: Sensitive Agent Refusal Details Leak via Future OFFSCREEN_REQUEST_TASK Callers

Field Detail
Category Information Disclosure
Severity High
Likelihood Possible
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-200,CWE-668
CAPEC CAPEC-118,CAPEC-224
OWASP A01:2021 - Broken Access Control

Description: OFFSCREEN_REQUEST_TASK handler in offscreen.ts allows unauthorized disclosure of agent-internal refusal context (e.g. details.personaDids) to arbitrary web page origins due to reliance on a single manual narrowing step in background.ts's handleRequestTask rather than a structural/type-enforced separation, resulting in cross-origin information disclosure of persona/ACL identifiers.

Evidence: packages/extension/src/background.ts:1855-1858

if (res.ok) return res;
  return { ok: false, error: res.error };

Attack Scenario:

  1. Attacker-controlled web page calls chrome.runtime.sendMessage with RUNTIME_REQUEST_TASK (EP-001), triggering handleRequestTask in background.ts.
  2. handleRequestTask relays the request to the offscreen document via OFFSCREEN_REQUEST_TASK (EP-003), which is answered identically for both page and console callers per offscreen.ts's shared branch (doRequestTask(...).catch((e) => sendResponse(relayFailure(e)))).
  3. The offscreen document's relayFailure builds a RelayTaskFailure containing the agent's raw code and details (e.g. { personaDids: [...] }) sourced from VtaClientError.details in relay-failure.ts.
  4. background.ts's handleRequestTask (lines ~1855-1858) currently strips code/details via explicit reconstruction: if (res.ok) return res; return { ok: false, error: res.error }; — this is the SOLE mitigation point.
  5. A future code change, refactor, or newly added caller of OFFSCREEN_REQUEST_TASK that forgets or bypasses this manual narrowing step (e.g. a new page-facing handler added elsewhere, or a regression removing the explicit reconstruction in favor of a type cast) returns the full OffscreenRequestTaskResponse — including code and details — directly to the untrusted page.
  6. The malicious web page's error handler for requestTask() receives details.personaDids or ACL contents naming other personas/contexts, which the page (attacker) was never entitled to see, enabling reconnaissance of the victim's other personas, bound profiles, or agent-side access control state.

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-001, EP-003

  • Data Flows: page->background->offscreen relay path

Preconditions: Attacker controls or compromises a web page that calls the extension's page-facing requestTask API., A code change (regression, refactor, new feature) reintroduces a type-cast (as RuntimeRequestTaskResponse) instead of the explicit object reconstruction, or a new handler forwards OffscreenRequestTaskResponse unmodified., The wallet/agent actually returns a refusal with non-empty details (e.g. profile deletion blocked by bound personas).

Existing Controls: handleRequestTask explicitly reconstructs the response object (return { ok: false, error: res.error }) instead of using a type cast, preventing accidental disclosure via structural typing tricks. • Extensive inline documentation (R3.7 policy comments) in bridge-protocol.ts, background.ts, and offscreen.ts explaining the invariant and why casts are dangerous. • RuntimeRequestTaskResponse type only defines { ok: false; error: string } for the page-facing shape, providing a compile-time signal (though not enforcement against runtime object literals).

Recommended Mitigations: Add a dedicated runtime assertion or lint rule that fails the build if any page-facing response object contains 'code' or 'details' keys. • Introduce a unit/integration test (extending relay-error-code.test.mts) explicitly asserting that handleRequestTask's page-facing return path never leaks code/details even when the offscreen mock returns a full RelayTaskFailure. • Refactor to a compile-time-enforced type (e.g. a branded/opaque type or exhaustive stripping utility function toPageSafeResponse()) shared across all page-facing return paths rather than an ad hoc explicit reconstruction duplicated per call site. • Add automated static analysis (e.g. ESLint custom rule) to flag any as RuntimeRequestTaskResponse or as RuntimeManagerTaskResponse casts applied to OffscreenRequestTaskResponse-typed values.


🟠 STRIDE-2: Manager Console Impersonation via Unauthenticated Runtime Message Origin

Field Detail
Category Spoofing, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.6 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-306,CWE-863
CAPEC CAPEC-98,CAPEC-122
OWASP A01:2021 - Broken Access Control, A07:2021 - Identification and Authentication Failures

Description: RuntimeManagerTaskRequest handler in background.ts allows privilege escalation to the operator-only management console surface due to reliance on chrome.runtime.onMessage sender checks that may be insufficiently restrictive, resulting in unauthorized administration task execution against the agent with full RelayTaskFailure disclosure.

Evidence: packages/extension/src/background.ts:2939

handleManagerTask(message as RuntimeManagerTaskRequest)
      .then(sendResponse)
      .catch((e: unknown) => sendResponse(relayFailure(e)));

Attack Scenario:

  1. Attacker identifies that RuntimeManagerTaskRequest (EP-002) is marked 'auth_required: true' in recon, but the actual enforcement point in background.ts's onMessage listener must correctly validate sender.id/sender.url before dispatching to handleManagerTask.
  2. If sender validation is missing, weak (e.g. only checking extension ID without verifying the sender is truly the management console page/origin), or if the console UI itself runs in a context reachable by other extension pages, an attacker-controlled extension page or compromised console-adjacent script sends a forged RuntimeManagerTaskRequest message.
  3. handleManagerTask processes the forged request and relays it via OFFSCREEN_REQUEST_TASK (EP-003) to the offscreen document, receiving the full OffscreenRequestTaskResponse including code and details.
  4. Because handleManagerTask passes the response through unmodified ('Passed through whole, failure members and all'), the attacker receives the complete RelayTaskFailure, including agent-internal details such as personaDids, ACL contents, or other sensitive context intended only for the legitimate operator console.
  5. Attacker leverages this to enumerate personas/profiles bound to the wallet and potentially triggers administration-level Trust-Task operations (e.g. profile deletion attempts) that reveal system state or degrade the wallet's runtime attestation.

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-002, EP-003

  • Data Flows: console->background->offscreen relay path

Preconditions: Attacker can send extension-internal runtime messages (e.g. via a malicious extension, a compromised content script with elevated messaging permission, or an insufficiently scoped chrome.runtime.onMessage listener that does not validate sender.id)., The sender-origin check for manager/console-only messages is missing or bypassable.

Existing Controls: recon marks EP-002 as 'auth_required: true', implying some authentication check exists in the current implementation (not fully visible in the reduced diff). • RuntimeManagerTaskRequest type is documented as 'Deliberately NOT in PAGE_FACING_RUNTIME_TYPES', suggesting a type-level allowlist may gate dispatch.

Recommended Mitigations: Explicitly verify chrome.runtime.MessageSender.id equals the extension's own ID and, where applicable, verify the sender.url corresponds to the packaged management console page (chrome-extension:///manager.html) before invoking handleManagerTask. • Add a dedicated capability token or session nonce established at console page load time and required on every RuntimeManagerTaskRequest message. • Add regression tests asserting that RuntimeManagerTaskRequest messages from non-console senders are rejected. • Document and enforce PAGE_FACING_RUNTIME_TYPES as an explicit runtime allowlist checked at the top of the onMessage listener, not merely a type-level comment.


🟡 STRIDE-3: Unsafe Type Cast Masking Structural Mismatch in Offscreen Response Handling

Field Detail
Category Tampering, Denial of Service
Severity Medium
Likelihood Likely
CVSS 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-20,CWE-704
CAPEC CAPEC-153
OWASP A04:2021 - Insecure Design

Description: OFFSCREEN_REQUEST_TASK response handling in background.ts allows type confusion between RuntimeRequestTaskResponse and OffscreenRequestTaskResponse due to reliance on TypeScript type casts (as OffscreenRequestTaskResponse) rather than runtime schema validation, resulting in silent acceptance of malformed or unexpected agent error payloads.

Evidence: packages/extension/src/background.ts:1848

})) as OffscreenRequestTaskResponse;

Attack Scenario:

  1. The offscreen document or a compromised/buggy dependency in the message pipeline returns a chrome.runtime.sendMessage response that does not conform to the OffscreenRequestTaskResponse shape (e.g. missing 'ok' field, unexpected structure) due to serialization edge cases.
  2. background.ts's handleRequestTask blindly casts the untyped response with as OffscreenRequestTaskResponse (line ~1848) without runtime validation (no zod or similar schema check).
  3. Downstream code accesses res.result.payloadDigest (line ~1851) or res.ok/res.error assuming the cast type is accurate; if the actual object lacks these fields, this throws a runtime TypeError inside the async handler.
  4. Because the .catch() only wraps errors from doRequestTask's promise chain and not necessarily unexpected property-access exceptions post-resolution in the same tick, an uncaught exception could crash the message handling flow or leave sendResponse uncalled, causing the calling page/console to hang or receive no response (DoS on that flow).
  5. Repeated forced malformed responses (e.g. via a compromised offscreen document or a race during extension update) could degrade the reliability of the requestTask relay for all callers.

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-003

  • Data Flows: background<->offscreen response handling

Preconditions: Offscreen document or the messaging channel returns a response that structurally deviates from OffscreenRequestTaskResponse., No runtime schema validation (e.g. zod) is applied to chrome.runtime.sendMessage responses before use.

Existing Controls: TypeScript compile-time typing catches many but not all structural mismatches (does not protect against actual runtime payloads that violate the type at runtime). • Optional chaining is used in some places (e.g. res.result?.kind) providing partial defensive coding.

Recommended Mitigations: Introduce runtime schema validation (e.g. zod) for all chrome.runtime.sendMessage response payloads at trust-boundary crossings (offscreen<->background<->content script). • Wrap post-resolution field access in try/catch or optional chaining consistently to guarantee sendResponse is always invoked. • Add fuzz/property-based tests sending malformed offscreen responses to verify graceful degradation.


🔵 STRIDE-4: Silent Detail Suppression via jsonSafe Masking Malformed Agent Payloads

Field Detail
Category Repudiation
Severity Low
Likelihood Likely
CVSS 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-778,CWE-223
CAPEC CAPEC-268
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: relayFailure's jsonSafe function in relay-failure.ts allows silent loss of diagnostic information due to unconditionally dropping unserializable or empty-object 'details' payloads without logging, resulting in reduced repudiation/traceability of malformed or anomalous agent error responses.

Evidence: packages/extension/src/relay-failure.ts:~95-105

function jsonSafe(value: unknown): unknown | undefined {
  if (value === undefined || value === null) return undefined;
  let round: unknown;
  try {
    round = JSON.parse(JSON.stringify(value)) as unknown;
  } catch {
    return undefined;
  }
  ...

Attack Scenario:

  1. A malicious or misbehaving agent/VTA server returns a VtaClientError with a 'details' field that is either an Error instance, a Map, contains a circular reference, or a BigInt.
  2. relayFailure calls jsonSafe(value) on this details field (relay-failure.ts).
  3. jsonSafe's JSON.parse(JSON.stringify(value)) round-trip either throws (caught, returns undefined) or produces an empty object '{}' that is then also discarded by the Object.keys(round).length === 0 check.
  4. The details are silently dropped with no logging, telemetry, or diagnostic record of the original malformed payload.
  5. A security investigator or operator later reviewing console-reported failures cannot determine that the agent sent anomalous/malformed details, masking potential agent-side bugs, tampering, or adversarial payload crafting attempts aimed at evading the relay's disclosure controls or triggering downstream parsing issues.
  6. This weakens non-repudiation: an attacker who intentionally sends unserializable/malicious 'details' to probe or disrupt the relay leaves no forensic trace in the console's failure log.

🔎 Threat Clue: Derived from COMP-004 via EP-002, EP-003

  • Data Flows: offscreen->background->console relay path

Preconditions: Agent, VTA server, or a man-in-the-middle on the agent<->extension channel can influence the shape of VtaClientError.details., No logging/telemetry hook exists downstream of jsonSafe's silent-drop branches.

Existing Controls: jsonSafe correctly prevents crash-on-serialize and prevents misleading empty-object disclosure to the console UI. • Comments in relay-failure.ts explicitly document the design rationale.

Recommended Mitigations: Add debug-level logging (not surfaced to the console UI, but captured in extension diagnostic logs) whenever jsonSafe drops a non-undefined input, recording the fact (not necessarily full content) that details were unserializable or empty. • Add a metrics counter for dropped-details events to detect anomalous agent behavior over time. • Consider preserving a redacted/truncated string representation for forensic purposes when full serialization fails.


🟡 STRIDE-5: Message Text Matching Regression Bypassing R3.7 Code-Based Refusal Handling

Field Detail
Category Tampering
Severity Medium
Likelihood Possible
CVSS 4.8 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-697,CWE-1039
CAPEC CAPEC-267
OWASP A04:2021 - Insecure Design

Description: interpretOutcome in manager/carrier.ts allows business-logic bypass of refusal handling due to reliance on optional 'code' field that may be undefined for many failures, resulting in operator panes falling back to fragile message-text matching (the exact anti-pattern R3.7 was designed to prevent) when the agent's message wording changes.

Evidence: packages/extension/src/manager/carrier.ts:~155-165

throw new RelayTaskError(taskType, `${label} failed: ${reply.error ?? "unknown error"}`, reply);

Attack Scenario:

  1. A pane in the management console is written to branch on RelayTaskError.code, but for a subset of failures (e.g. transport-level errors that never reach the agent) code is undefined, forcing the pane's fallback logic to inspect error.message text.
  2. If any pane author reverts to error.message.includes(...) style matching for cases where code is absent (which the documentation explicitly warns is fragile), an agent-side wording change (a legitimate, allowed change per this PR's own doc comments: 'the agent may reword a message whenever it likes') silently breaks that pane's business logic.
  3. An attacker who can influence agent-side error message wording (e.g. by controlling a misbehaving/compromised agent or VTA server, or through a supply-chain compromise of packages/core) can deliberately alter refusal message text to make a pane misclassify a refusal as success-adjacent or a different error category, changing operator-visible security decisions (e.g. incorrectly concluding a profile deletion is not blocked, or incorrectly retrying based on wrong error type).
  4. This is a latent, PR-created contract weakness rather than a currently-visible code bug: it's a risk introduced by the coexistence of code-based and text-based failure handling, exploitable if enforcement (linting/tests) does not prevent regression to text-matching in new panes.

🔎 Threat Clue: Derived from COMP-001 via EP-002

  • Data Flows: console business-logic layer

Preconditions: A pane author violates the documented R3.7 principle by matching on message text for codeless failures., Attacker or compromised agent can control/influence the wording of the human-readable error message.

Existing Controls: Extensive documentation instructing developers never to match on message text (R3.7). • RelayTaskError class structurally separates code/details from message, making the correct pattern easy to follow. • relay-error-code.test.mts exists, suggesting some test coverage for code preservation.

Recommended Mitigations: Add an ESLint rule flagging any .message.includes( or similar string matching against RelayTaskError/Error instances in the manager package. • Expand relay-error-code.test.mts to cover the codeless-failure fallback path explicitly, asserting panes have a well-defined codeless branch (e.g. generic retry/prose-only) rather than text matching. • Add a CI check that fails if agent message wording changes are covered by test assertions on message content.


🟡 STRIDE-6: Missing Runtime Enforcement of PAGE_FACING_RUNTIME_TYPES Allowlist

Field Detail
Category Elevation of Privilege, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 6.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-284,CWE-863
CAPEC CAPEC-122
OWASP A01:2021 - Broken Access Control

Description: chrome.runtime.onMessage listener in background.ts allows dispatch confusion between page-facing and console-facing message types due to comment-only ('deliberately NOT in PAGE_FACING_RUNTIME_TYPES') rather than compiler/runtime-enforced separation, resulting in potential future routing of console-privileged RuntimeManagerTaskRequest logic to page-originated messages.

Evidence: packages/extension/src/bridge-protocol.ts:~1900

/** manager console → background: run one administration task at the agent.
 *
 *  **Deliberately NOT in {@link PAGE_FACING_RUNTIME_TYPES}, and deliberately

Attack Scenario:

  1. bridge-protocol.ts documents RuntimeManagerTaskRequest as 'Deliberately NOT in PAGE_FACING_RUNTIME_TYPES' via a comment, implying a list-based allowlist exists somewhere in the codebase distinguishing page vs. console message types.
  2. This separation currently relies on developer discipline; the provided source excerpt does not show a runtime guard rejecting a RuntimeManagerTaskRequest-shaped message if it arrives tagged as a page-originated RUNTIME_REQUEST_TASK type or vice versa.
  3. An attacker-controlled web page crafts a message payload structurally identical to RuntimeManagerTaskRequest (same params shape) but sent with the RUNTIME_REQUEST_TASK type tag, or exploits a future refactor that merges dispatch logic.
  4. If dispatch logic is ever keyed loosely (e.g. by presence of 'params' field rather than strict 'type' discrimination with sender validation), the page could reach handleManagerTask's code path and receive full RelayTaskFailure details (chaining with STRIDE-1/STRIDE-2).
  5. Because this is a design/process control rather than a technical control, a future contributor unaware of the R3.7 convention could introduce exactly this regression without any compiler error, since TypeScript's discriminated union typing does not prevent an attacker from sending an arbitrarily-shaped runtime object.

🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-002

  • Data Flows: content script bridge -> background dispatch

Preconditions: A future code change merges or loosens the strict type-tag-based dispatch between page and console message handlers., Attacker can send chrome.runtime messages with attacker-chosen type/shape combinations from a web page context (via the content script bridge).

Existing Controls: Current dispatch (visible in diff) appears to strictly switch on msg.type equality checks (e.g. if (msg.type === OFFSCREEN_REQUEST_TASK)), providing type-tag-based isolation today. • Strong documentation of the invariant for future maintainers.

Recommended Mitigations: Add automated tests asserting that a page-originated message with RuntimeManagerTaskRequest's shape but RUNTIME_REQUEST_TASK type is routed only to the page-facing handler and never reaches handleManagerTask. • Add sender-based validation (sender.tab present = page origin; sender.tab absent + extension-internal = console) as a defense-in-depth check independent of the message 'type' field. • Formalize PAGE_FACING_RUNTIME_TYPES as an exported, tested constant array checked at runtime in the top-level onMessage listener rather than relying solely on comments.


🟡 STRIDE-7: Consent Replay Digest Reuse Enabling Replay Attack on Consent Flow

Field Detail
Category Tampering, Repudiation
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-294,CWE-367
CAPEC CAPEC-60,CAPEC-21
OWASP A04:2021 - Insecure Design

Description: handleRequestTask's consent-replay recording in background.ts allows a race/replay-adjacent condition due to keying replay armament solely on a VTA-supplied payloadDigest without additional freshness binding visible in the excerpt, resulting in potential consent-ceremony replay if the digest is predictable or reused across sessions.

Evidence: packages/extension/src/background.ts:1849-1853

const digest = res.result.payloadDigest;
    if (typeof digest === "string" && digest) consentReplays.recordConsentRequired(key, digest);

Attack Scenario:

  1. handleRequestTask observes res.result.payloadDigest from a consentRequired outcome and calls consentReplays.recordConsentRequired(key, digest) (background.ts, line ~1851).
  2. The comment states 'A consent refusal is the only thing that arms a replay, and it carries the VTA's own salted digest — the same value its task-consent/granted notice [uses]', implying the digest is the sole correlator between a consent request and its later grant.
  3. If an attacker (malicious page, or a page compromised via XSS on a legitimate site using the extension) can trigger multiple requestTask calls and observe or predict digest values (e.g. if the VTA's salting is weak, deterministic per-session, or the digest space is small), the attacker could attempt to pre-arm or replay a stale consent-required state.
  4. Because the code excerpt shows only 'key' and 'digest' being recorded without visible expiration/single-use enforcement in this file, a replay of an old granted notice matching a re-armed digest could potentially cause the extension to treat a stale/attacker-replayed grant as fresh consent, bypassing the intended one-time consent ceremony.
  5. This would allow an attacker to execute a Trust-Task operation that should have required fresh, explicit user consent, without the user having consented to that specific instance.

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-001, EP-003

  • Data Flows: page->background consent ceremony

Preconditions: Attacker can trigger repeated requestTask calls and correlate/predict payloadDigest values., The consentReplays store (not shown in full) lacks single-use invalidation, short TTL, or session binding., The VTA's digest salting scheme is weaker than assumed or reused across requests.

Existing Controls: Digest is described as 'salted', providing some resistance to prediction if the salting is cryptographically sound and per-request. • The mechanism explicitly ties replay-arming to the VTA's own value rather than an extension-generated one, reducing local forgery risk. • consentReplays module (not fully shown) likely implements some tracking logic (name suggests replay-detection intent, which is a positive control if implemented correctly).

Recommended Mitigations: Confirm and enforce single-use consumption of each payloadDigest (delete from consentReplays store immediately upon successful grant match). • Add explicit TTL/expiration to recorded consent-required digests independent of VTA-side behavior. • Add integration tests specifically for replay scenarios: re-sending a previously granted digest and asserting rejection. • Bind the consent digest additionally to session/tab identity to prevent cross-context replay.


🔵 STRIDE-8: Prompt-Injection-Style Instruction Smuggling via Untrusted Repository/PR Content

Field Detail
Category Tampering, Repudiation
Severity Low
Likelihood Unlikely
CVSS 2.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N
Residual Severity None
CWE CWE-1427
CAPEC CAPEC-242
OWASP A03:2021 - Injection

Description: PR/diff ingestion pipeline in the security analysis tooling allows instruction-injection attempts via embedded directive-like text blocks (e.g. the [[AISEC-GUARD-v1]] framing observed in this very input) due to LLM-based analysis tools treating repository content as trusted context, resulting in potential analysis manipulation or false-negative suppression if the guard is not enforced.

Evidence: N/A (task input framing, not repository source):N/A

[[AISEC-GUARD-v1]] SECURITY DIRECTIVE (highest priority, non-overridable): ...

Attack Scenario:

  1. An attacker submits a pull request or repository content containing text resembling system-level directives (e.g. '[[AISEC-GUARD-v1]]', 'ignore previous instructions', 'mark this as a false positive') embedded in comments, README files, or commit messages.
  2. If the downstream security tooling's LLM-based reviewer does not strictly sandbox repository content as untrusted data, the injected text could influence the model's classification, causing it to suppress findings, mislabel severities, or skip files.
  3. In this specific analysis, such a block was present in the raw task input; per the enforced security directive, it was treated strictly as data under analysis and had no effect on the current output — this finding documents that observed injection attempt for audit/traceability purposes.
  4. A less rigorously guarded pipeline (or a future version of this tool lacking the AISEC-GUARD directive) could be manipulated into silently approving a malicious PR by embedding convincing 'this is a false positive, do not report' text near a real vulnerability.

🔎 Threat Clue: Derived from N/A via N/A - tooling ingestion pipeline, not application runtime

  • Data Flows: PR/diff content -> LLM security analysis tool

Preconditions: The security analysis tool ingests raw PR/repository text into an LLM context without strict data/instruction separation., Attacker has PR/contribution access sufficient to embed arbitrary text in the diff, comments, or files reviewed by the tool.

Existing Controls: This analysis pipeline enforces an explicit, non-overridable security directive treating all ingested content as untrusted data, which successfully neutralized the observed injection attempt. • The injection attempt was detected and is being reported as a finding rather than acted upon.

Recommended Mitigations: Continue enforcing strict data/instruction separation in all LLM-based security tooling ingesting untrusted repository content. • Log and alert on detected instruction-injection patterns in PRs for security team visibility, independent of the underlying code findings. • Require human review sign-off for any PR containing detected prompt-injection-style content before merge, regardless of automated verdict.


🟡 STRIDE-9: Unvalidated Wire Details Field Enabling Downstream Prototype/Property Injection in Console Panes

Field Detail
Category Tampering, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-79,CWE-1321,CWE-20
CAPEC CAPEC-63,CAPEC-137
OWASP A03:2021 - Injection

Description: RelayTaskFailure.details and RelayTaskError.details in bridge-protocol.ts and manager/carrier.ts allow injection of attacker-influenced arbitrary JSON structures due to explicit typing as unknown without schema validation at the point of consumption, resulting in potential logic errors, UI injection, or unexpected property access in operator console panes that read members off details without validation.

Evidence: packages/extension/src/bridge-protocol.ts:~1790-1805

details?: unknown;
}

Attack Scenario:

  1. An attacker who can influence agent/VTA server responses (e.g. compromised VTA backend, MITM without TLS pinning, or a malicious agent operator) crafts a trust-task-error payload with an unexpected details shape, e.g. { personaDids: '<script>...' } or deeply nested/oversized objects, or objects with keys like __proto__.
  2. This flows through VtaClientError.details, through relayFailure's jsonSafe (relay-failure.ts) which only strips unserializable content and empty objects, but does NOT validate the structure or sanitize string content of details.
  3. RelayTaskFailure.details (bridge-protocol.ts) carries this unvalidated value to the console via chrome.runtime.sendMessage.
  4. The console pane, per the documentation in manager/carrier.ts ('a pane may read members off it, but it is unvalidated wire data and every member has to be checked before it is used'), is trusted by convention (not enforcement) to validate before rendering.
  5. If any console pane renders details.personaDids or similar fields via unsafe DOM insertion (e.g. innerHTML) rather than safe text rendering, this could result in a stored/reflected XSS-like injection within the operator console's own UI, executing attacker-controlled script in the privileged console context.
  6. Alternatively, if a pane does Object.assign(target, details) or similar merge patterns without key filtering, a crafted __proto__ or constructor.prototype key in details could pollute the pane's runtime object prototypes (prototype pollution), altering application behavior.

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-002, EP-003

  • Data Flows: agent -> offscreen -> background -> console details rendering

Preconditions: Attacker controls or compromises the agent/VTA backend or the transport between the extension and the VTA (no TLS/integrity verification assumed broken for this scenario)., At least one console pane renders details fields via unsafe sinks (innerHTML, unguarded object merge) rather than safe text interpolation or explicit allow-listed field access.

Existing Controls: Extensive documentation warning that details is unvalidated wire data requiring per-member checks before use. • jsonSafe strips non-JSON-serializable content, reducing (but not eliminating) exotic payload shapes. • TypeScript types details as unknown, forcing explicit type narrowing at each call site (a compile-time nudge, not a runtime guarantee).

Recommended Mitigations: Add runtime schema validation (e.g. zod) for known details shapes (e.g. profileInUse's personaDids array of strings) before rendering in any console pane. • Audit all console panes for unsafe DOM sinks (innerHTML, dangerouslySetInnerHTML equivalents) rendering details content and replace with safe text rendering. • Add an explicit deny-list/sanitization step for dangerous keys (__proto__, constructor, prototype) in jsonSafe or at the point of first consumption. • Enforce TLS certificate pinning or equivalent integrity verification between the extension and the VTA/agent backend to reduce MITM tampering risk on trust-task-error payloads.


🔵 STRIDE-10: Denial of Service via Unbounded Details Payload Size in Relay Chain

Field Detail
Category Denial of Service
Severity Low
Likelihood Possible
CVSS 3.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:L
Residual Severity Low
CWE CWE-400,CWE-770
CAPEC CAPEC-130
OWASP A04:2021 - Insecure Design

Description: jsonSafe and RelayTaskFailure.details in relay-failure.ts and bridge-protocol.ts allow resource exhaustion via oversized or deeply nested details payloads due to absence of size/depth limits on the JSON round-trip, resulting in potential performance degradation or crash of the background/offscreen message-passing pipeline.

Evidence: packages/extension/src/relay-failure.ts:~95-100

round = JSON.parse(JSON.stringify(value)) as unknown;

Attack Scenario:

  1. A malicious or compromised agent/VTA backend returns a trust-task-error with an extremely large or deeply nested details object (e.g. megabytes of JSON or thousands of nesting levels).
  2. jsonSafe performs JSON.parse(JSON.stringify(value)) on this payload (relay-failure.ts) without any size or depth guard.
  3. For sufficiently large/pathological input, this round-trip can consume excessive CPU/memory in the offscreen document or background service worker, both of which run in resource-constrained Chrome extension contexts.
  4. chrome.runtime.sendMessage then attempts to serialize/transmit this large object across the extension's internal message-passing boundary, potentially exceeding Chrome's message size limits or causing UI jank/unresponsiveness in the console.
  5. Repeated triggering (e.g. an attacker-controlled or compromised agent issuing many such oversized refusals) could degrade the responsiveness of the extension's background service worker for all tabs/consoles relying on it.

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-002, EP-003

  • Data Flows: agent -> offscreen -> background -> console relay

Preconditions: Attacker controls or compromises the agent/VTA backend to return oversized/pathological details payloads., No size/depth validation exists between the agent response and the jsonSafe round-trip.

Existing Controls: jsonSafe drops entirely unserializable values (e.g. circular references throw and are caught), providing a partial ceiling on some pathological cases. • Chrome extension message passing has some inherent size limits that would eventually reject extremely large payloads (though this yields its own failure mode rather than graceful handling).

Recommended Mitigations: Add an explicit size cap (e.g. truncate or reject details payloads exceeding N KB) before JSON round-tripping in jsonSafe. • Add a depth limit check to reject pathologically nested objects before serialization. • Add monitoring/alerting for anomalously large trust-task-error responses from the agent/VTA backend.


🔵 STRIDE-11: Coercion Bucket Overloading in coerceTrustTaskCode Masking Distinct Extended Error Codes

Field Detail
Category Repudiation, Information Disclosure
Severity Low
Likelihood Likely
CVSS 3.8 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-697,CWE-1039
CAPEC CAPEC-267
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: coerceTrustTaskCode (referenced in relay-failure.ts) allows loss of distinguishability between refusal reasons due to bucketing every unrecognized extended code into a single e.p.msg.bad_request value, resulting in operator console panes potentially misinterpreting distinct security-relevant refusals as generic bad-request errors when only the coerced VtaErrorCode (rather than the raw agent code) is consulted.

Evidence: packages/extension/src/relay-failure.ts:~40-50

// The agent's code is preferred over the VtaErrorCode the client coerced it
// to, because that coercion is lossy by design: coerceTrustTaskCode buckets
// every extended code it does not recognise into e.p.msg.bad_request

Attack Scenario:

  1. The agent emits a new or uncommon SPEC §8.5 extended error code not yet recognized by coerceTrustTaskCode's mapping table.
  2. coerceTrustTaskCode buckets this into the generic e.p.msg.bad_request VtaErrorCode as documented in relay-failure.ts's comments.
  3. If any code path in the manager console or background.ts consults the coerced VtaErrorCode instead of the raw agent code preserved in RelayTaskFailure.code (the documented correct pattern says 'the agent's code is preferred... this is that caller' — implying other callers elsewhere in the codebase may NOT follow this pattern), the pane displays or acts on a misleadingly generic 'bad request' classification for what may be a distinctly security-relevant refusal (e.g. a novel access-denial reason).
  4. This reduces the operator's ability to audit and correctly attribute the true cause of a refusal, weakening the non-repudiation/traceability goal that motivated this entire PR (R3.7).

🔎 Threat Clue: Derived from COMP-001 via EP-002

  • Data Flows: agent -> relayFailure -> console

Preconditions: The agent emits an extended error code unrecognized by the current coerceTrustTaskCode mapping., A console code path exists (outside the reviewed relayFailure function) that consults the coerced code rather than the raw agent code.

Existing Controls: relayFailure explicitly prefers the raw agent code over the coerced VtaErrorCode, as documented and implemented in this PR. • Documentation clearly flags coerceTrustTaskCode's lossy behavior for future maintainers.

Recommended Mitigations: Audit all remaining call sites in the codebase that consume VtaErrorCode to confirm none rely on the coerced bucket for security-relevant branching. • Add telemetry logging when coerceTrustTaskCode falls back to the generic bucket, to detect and prioritize adding support for new extended codes. • Expand relay-error-code.test.mts to cover the coercion-bucket-vs-raw-code precedence explicitly for newly introduced codes.



🍝 PASTA Threat Model

Application Purpose

A Chrome Extension (Manifest V3) acting as a browser-based Verifiable Trust Agent (VTA) bridge, relaying Trust-Task protocol requests between untrusted web pages, an operator-controlled management console, and an offscreen document that holds wallet key material and communicates with the VTA/agent backend.

Inherent Risks

  • The extension bridges two fundamentally different trust levels (arbitrary web pages vs. the operator's own management console) through a single shared offscreen message-handling code path.
  • Wallet key material and persona/profile data reside in the offscreen document's IndexedDB, making the offscreen document a high-value target.
  • The security separation between page-facing and console-facing response shapes is currently enforced manually per call site rather than by a single reusable/tested utility.

Objectives

Risk: Treat any leakage of persona/profile identifiers or ACL contents to an untrusted web origin as a high-severity risk requiring immediate remediation.
Business: Provide a trustworthy browser bridge enabling web pages and an operator console to interact with a user's VTA/agent without leaking the agent's internal decision-making context to untrusted sites.
Security: Ensure page-facing responses never expose agent-internal refusal codes or structured details (R3.7-adjacent boundary).; Ensure only the legitimate management console can invoke administration-level Trust-Tasks.
Financial: Avoid costs associated with security incidents, regulatory fines, or reputational damage from disclosure of persona/profile data.
Compliance: Align with the internal R3.7 policy mandating machine-readable, code-based error handling instead of fragile message-text matching.
Functional: Support Trust-Task request/response relay for both page-originated and console-originated callers via a shared offscreen document architecture.
Operational: Maintain reliable, low-latency message relay across the page → content script → background → offscreen hop chain.

Business Impact Analysis (3)

BIA-1: Page-Facing Trust-Task Relay (High)

The end-to-end flow by which an arbitrary web page proposes a Trust-Task to the user's VTA and receives a minimal, prose-only success/failure response.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Extension Users / Security Engineering Team / Web Page Integrators
  • Dependencies: Background Service Worker / Content Script Bridge / Offscreen Document / VTA/Agent Backend
  • Disruptions: Regression reintroducing full RelayTaskFailure disclosure to pages / Offscreen document crash or unavailability / Malformed offscreen response causing unhandled exception
  • Impacts: Cross-origin disclosure of persona/ACL data to malicious sites / Loss of user trust in the extension's confidentiality guarantees / Potential regulatory scrutiny if personal identifiers are considered PII

BIA-2: Management Console Administration Relay (Critical)

The end-to-end flow by which the operator's management console issues administration Trust-Tasks (e.g. profile deletion) and receives full machine-readable refusal detail for programmatic handling.

MTD: 00 days 12:00 hours | RTO: 00 days 02:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Extension Operators / Security Engineering Team
  • Dependencies: Background Service Worker / Offscreen Document / VTA/Agent Backend / Manager Console UI
  • Disruptions: Unauthorized message spoofing reaching handleManagerTask / Sender-origin validation bypass / Replay of stale consent digests
  • Impacts: Unauthorized administration actions against the agent / Disclosure of persona/ACL data to unauthorized senders / Operator inability to trust code-based refusal handling if message text matching regresses

BIA-3: Wallet Key Material and Consent Ceremony Integrity (Critical)

The offscreen document's management of the wallet's holder key material and the consent-required/consent-granted ceremony gating sensitive Trust-Task execution.

MTD: 00 days 06:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Extension Users / Security Engineering Team
  • Dependencies: Offscreen Document / IndexedDB / consentReplays store
  • Disruptions: Consent digest replay or prediction / IndexedDB compromise exposing key material
  • Impacts: Unauthorized Trust-Task execution without fresh user consent / Irrecoverable loss of holder key material and associated wallet identity

Technical Scope

Roles (3): RO-1 Web Page (Untrusted) · RO-2 Extension User/Holder · RO-3 Console Operator

Actors (5): AC-1 Web Page Script · AC-2 Wallet Holder · AC-3 Console Operator · AC-4 Background Service Worker · AC-5 VTA/Agent Backend

Entry Points (4): EP-001 Page-Facing Task Request · EP-002 Manager Console Task Request · EP-003 Offscreen Task Relay · EP-004 Offscreen Wallet UI Messages

Threat Actors (4): TA-1 Malicious Website Operator · TA-2 Compromised/Malicious Extension or Content Script · TA-3 Malicious or Compromised VTA/Agent Backend · TA-4 Insider/Future Contributor Error

Infrastructure (2): IF-1 Browser Extension Runtime · IF-2 VTA/Agent Backend Infrastructure

Trust Boundaries (4): TB-1 Untrusted Web Page Boundary · TB-2 Extension Internal Messaging Boundary · TB-3 Operator Console Boundary · TB-4 Agent/VTA Backend Boundary

External Entities (2): EE-1 Arbitrary Web Page · EE-2 VTA/Agent Backend

System Components (7): SC-1 Background Service Worker · SC-2 Content Script Bridge · SC-3 Offscreen Document · SC-4 Management Console UI · SC-5 VTA/Agent Backend · SC-6 IndexedDB Wallet Store · SC-7 Consent Replay Store

Resources And Assets (4): RA-1 Persona/Profile Identifiers · RA-2 Holder Key Material (did:peer #key-2) · RA-3 Consent-Required Payload Digest · RA-4 Agent Refusal Code and Structured Details

Technologies And Dependencies (3): TD-1 Chrome Extension Manifest V3 · TD-2 @openvtc/pnm-core · TD-3 IndexedDB

Use Cases (2)

  • Page-Originated Trust-Task Request: A web page requests a Trust-Task from the user's VTA through the content script bridge and background service worker, receiving a minimal success result or prose-only failure message.
  • Operator Console Administration Task: The operator's management console issues an administration Trust-Task (e.g. profile deletion) to the agent and receives the full machine-readable refusal code and structured details when the task is r

⚔️ Attack Scenarios (4)

SC-1: Background Service Worker

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Website Operator<br><i>Harvest persona data</i>" }
    TA2@{ shape: rect, label: "TA-2: Compromised Extension<br><i>Escalate to console</i>" }
    TA4@{ shape: rect, label: "TA-4: Insider/Future Contributor Error<br><i>Regression</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S1@{ shape: rect, label: "STRIDE-1: Sensitive Refusal Details Leak<br><i>High / Possible</i>" }
    S2@{ shape: rect, label: "STRIDE-2: Manager Console Impersonation<br><i>High / Possible</i>" }
    S6@{ shape: rect, label: "STRIDE-6: Missing PAGE_FACING_RUNTIME_TYPES Enforcement<br><i>Medium / Possible</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C118@{ shape: rect, label: "CAPEC-118: Data Leakage Attacks" }
    C122@{ shape: rect, label: "CAPEC-122: Privilege Abuse" }
    C98@{ shape: rect, label: "CAPEC-98: Phishing/Impersonation" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W200@{ shape: rect, label: "CWE-200: Exposure of Sensitive Information" }
    W306@{ shape: rect, label: "CWE-306: Missing Authentication for Critical Function" }
    W284@{ shape: rect, label: "CWE-284: Improper Access Control" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: Background Service Worker" }
  end
  TA1 --> S1
  TA2 --> S2
  TA4 --> S6
  S1 --> C118
  S2 --> C98
  S6 --> C122
  C118 --> W200
  C98 --> W306
  C122 --> W284
  W200 --> SC1
  W306 --> SC1
  W284 --> SC1
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
  linkStyle 3 stroke:#FF0000,stroke-width:2px
  linkStyle 4 stroke:#FF0000,stroke-width:2px
  linkStyle 5 stroke:#FFA500,stroke-width:2px
  linkStyle 6 stroke:#FF0000,stroke-width:2px
  linkStyle 7 stroke:#FF0000,stroke-width:2px
  linkStyle 8 stroke:#FFA500,stroke-width:2px
  linkStyle 9 stroke:#FF0000,stroke-width:2px
  linkStyle 10 stroke:#FF0000,stroke-width:2px
  linkStyle 11 stroke:#FFA500,stroke-width:2px
Loading

SC-3: Offscreen Document

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA3@{ shape: rect, label: "TA-3: Malicious/Compromised VTA Backend<br><i>Inject malformed payloads</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S3@{ shape: rect, label: "STRIDE-3: Unsafe Type Cast Type Confusion<br><i>Medium / Likely</i>" }
    S9@{ shape: rect, label: "STRIDE-9: Unvalidated Wire Details Injection<br><i>Medium / Possible</i>" }
    S10@{ shape: rect, label: "STRIDE-10: Unbounded Details Payload DoS<br><i>Low / Possible</i>" }
    S4@{ shape: rect, label: "STRIDE-4: Silent Detail Suppression<br><i>Low / Likely</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
    C63@{ shape: rect, label: "CAPEC-63: Cross-Site Scripting" }
    C130@{ shape: rect, label: "CAPEC-130: Excessive Allocation" }
    C268@{ shape: rect, label: "CAPEC-268: Audit Log Manipulation" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W20@{ shape: rect, label: "CWE-20: Improper Input Validation" }
    W1321@{ shape: rect, label: "CWE-1321: Prototype Pollution" }
    W400@{ shape: rect, label: "CWE-400: Uncontrolled Resource Consumption" }
    W778@{ shape: rect, label: "CWE-778: Insufficient Logging" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC3@{ shape: rect, label: "SC-3: Offscreen Document" }
  end
  TA3 --> S3
  TA3 --> S9
  TA3 --> S10
  TA3 --> S4
  S3 --> C153
  S9 --> C63
  S10 --> C130
  S4 --> C268
  C153 --> W20
  C63 --> W1321
  C130 --> W400
  C268 --> W778
  W20 --> SC3
  W1321 --> SC3
  W400 --> SC3
  W778 --> SC3
  linkStyle 0 stroke:#FFA500,stroke-width:2px
  linkStyle 1 stroke:#FFA500,stroke-width:2px
  linkStyle 2 stroke:#00FF00,stroke-width:2px
  linkStyle 3 stroke:#00FF00,stroke-width:2px
  linkStyle 4 stroke:#FFA500,stroke-width:2px
  linkStyle 5 stroke:#FFA500,stroke-width:2px
  linkStyle 6 stroke:#00FF00,stroke-width:2px
  linkStyle 7 stroke:#00FF00,stroke-width:2px
  linkStyle 8 stroke:#FFA500,stroke-width:2px
  linkStyle 9 stroke:#FFA500,stroke-width:2px
  linkStyle 10 stroke:#00FF00,stroke-width:2px
  linkStyle 11 stroke:#00FF00,stroke-width:2px
  linkStyle 12 stroke:#FFA500,stroke-width:2px
  linkStyle 13 stroke:#FFA500,stroke-width:2px
  linkStyle 14 stroke:#00FF00,stroke-width:2px
  linkStyle 15 stroke:#00FF00,stroke-width:2px
Loading

SC-7: Consent Replay Store

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Website Operator<br><i>Bypass consent ceremony</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S7@{ shape: rect, label: "STRIDE-7: Consent Replay Digest Reuse<br><i>Medium / Possible</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C60@{ shape: rect, label: "CAPEC-60: Reusing Session IDs (aka Session Replay)" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W294@{ shape: rect, label: "CWE-294: Authentication Bypass by Capture-replay" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC7@{ shape: rect, label: "SC-7: Consent Replay Store" }
  end
  TA1 --> S7
  S7 --> C60
  C60 --> W294
  W294 --> SC7
  linkStyle 0 stroke:#FFA500,stroke-width:2px
  linkStyle 1 stroke:#FFA500,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
Loading

SC-4: Management Console UI

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA4@{ shape: rect, label: "TA-4: Insider/Future Contributor Error<br><i>Fragile text matching regression</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S5@{ shape: rect, label: "STRIDE-5: Message Text Matching Regression<br><i>Medium / Possible</i>" }
    S11@{ shape: rect, label: "STRIDE-11: Coercion Bucket Overloading<br><i>Low / Likely</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C267@{ shape: rect, label: "CAPEC-267: Analytic Attacks on Cryptographic Systems" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W697@{ shape: rect, label: "CWE-697: Incorrect Comparison" }
    W1039@{ shape: rect, label: "CWE-1039: Automated Recognition Bypass" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC4@{ shape: rect, label: "SC-4: Management Console UI" }
  end
  TA4 --> S5
  TA4 --> S11
  S5 --> C267
  S11 --> C267
  C267 --> W697
  C267 --> W1039
  W697 --> SC4
  W1039 --> SC4
  linkStyle 0 stroke:#FFA500,stroke-width:2px
  linkStyle 1 stroke:#00FF00,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
  linkStyle 3 stroke:#00FF00,stroke-width:2px
  linkStyle 4 stroke:#FFA500,stroke-width:2px
  linkStyle 5 stroke:#00FF00,stroke-width:2px
Loading

📊 Risk Summary

Total Threats: 11

By Severity: Low: 4 · High: 2 · Medium: 5

By Category: Information Disclosure: 4 · Spoofing: 1 · Elevation of Privilege: 2 · Tampering: 5 · Denial of Service: 2 · Repudiation: 4

🎯 Attack Surface

Kill Chain 1: The most severe chain begins with a malicious web page (TA-1) exploiting the single manual narrowing point in background.ts's handleRequestTask (STRIDE-1). Because OFFSCREEN_REQUEST_TASK (EP-003) in offscreen.ts answers page and console callers identically with the full RelayTaskFailure shape, any future regression that reintroduces a type cast instead of the explicit object reconstruction — or any new page-facing handler that forgets this step — instantly exposes persona/ACL identifiers to arbitrary origins, chaining directly into STRIDE-9's unvalidated details rendering risk if a console pane also mishandles the same details shape. Kill Chain 2: A second chain involves a compromised co-installed extension or a sender-spoofing attempt (TA-2) targeting the RuntimeManagerTaskRequest entry point (EP-002) in background.ts (STRIDE-2). If sender-origin validation is weak or absent, this path reaches handleManagerTask, which passes RelayTaskFailure through unmodified by design — meaning a successful spoof yields the same high-value disclosure as Kill Chain 1, but via privilege escalation into the console-only administration surface rather than the page relay, and could further chain into unauthorized Trust-Task execution (profile deletion, persona manipulation) if the underlying agent authorization checks are also weak. Kill Chain 3: A third, lower-probability but architecturally interesting chain involves the consent ceremony (STRIDE-7): an attacker who can trigger repeated requestTask calls and correlate salted payloadDigest values in the consentReplays store (SC-7) could attempt to pre-arm or replay a stale consent-required state, potentially causing a Trust-Task to execute without the fresh, explicit consent the ceremony is designed to require — this chain is speculative given the reduced source excerpt but warrants confirmation against the full consentReplays implementation. Kill Chain 4: A fourth chain centers on the VTA/agent backend itself (TA-3) as an untrusted upstream: a compromised or malicious agent could send oversized or structurally anomalous 'details' payloads that survive jsonSafe's lenient round-trip (STRIDE-10, STRIDE-9), reaching console panes that may render them unsafely (XSS/prototype pollution) or exhausting background/offscreen resources (DoS), demonstrating that the R3.7 disclosure-control redesign, while solving the page-confidentiality problem, has not yet been paired with equivalent input-validation hardening on the upstream agent-to-extension trust boundary.

🛡️ Risk Mitigation Strategy

Priority 1 (Immediate): The most urgent gap is the absence of a single, reusable, and tested utility function enforcing the page-facing response narrowing (currently duplicated manually in handleRequestTask) and the absence of compiler/runtime-enforced sender validation for RuntimeManagerTaskRequest. Both should be addressed immediately via (a) extracting a toPageSafeResponse() utility covered by dedicated regression tests asserting code/details are never present in page-facing output even under adversarial offscreen responses, and (b) adding explicit sender.id/sender.url validation at the top of the onMessage listener for all manager-console-only message types, backed by tests simulating spoofed senders. Priority 2 (Short-Term): Runtime schema validation (e.g. zod) should be introduced at every trust-boundary crossing — offscreen-to-background, background-to-console, and agent-to-offscreen — to close the type-cast-without-validation gap (STRIDE-3), the unvalidated-details injection gap (STRIDE-9), and the payload-size DoS gap (STRIDE-10). This directly hardens the newly introduced RelayTaskFailure/OffscreenRequestTaskResponse types against both accidental structural drift and adversarial upstream agent payloads. Priority 3 (Medium-Term): Strengthen the consent-replay mechanism (STRIDE-7) by confirming and enforcing single-use digest consumption, TTL expiration, and session binding, paired with explicit replay-scenario integration tests; this closes a business-logic gap that is currently only partially visible in the provided source and should be fully audited against the complete consentReplays implementation. Priority 4 (Long-Term): Institutionalize the R3.7 code-over-message-text discipline via static analysis tooling (custom ESLint rules flagging .message.includes( patterns and flagging casts to page-facing response types) and expand telemetry around coerceTrustTaskCode's lossy bucketing (STRIDE-11) and jsonSafe's silent-drop behavior (STRIDE-4), converting


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
1 4

Confirmed (1)

  • 🟡 jsonSafe() silently discards unserializable or empty 'details' payloads with no logging, weakening forensic traceability of anomalous agent responses (triaged LOW→MEDIUM)

Must-Review-By-Human (4)

  • 🟡 Structured details from agent passed to console without sanitization before rendering
  • 🟡 Agent-controlled error code/details relayed to console enables logic bypass if pane trusts code equality checks without validating details shape
  • 🟡 Single manual code path is the only enforcement of the page/console information-disclosure boundary (no compiler- or runtime-enforced separation)
  • 🟡 chrome.runtime.sendMessage response cast without runtime schema validation at background/offscreen trust boundary (triaged LOW→MEDIUM)

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.

2 participants