feat(console): a rejection keeps its code across the bridge - #171
Conversation
`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>
🛡️ AI Agentic Security Code Review1 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
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 6 · findings: 7
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (1)🟡 jsonSafe() silently discards unserializable or empty 'details' payloads with no logging, weakening forensic traceability of anomalous agent responses
🧠 AI Triage:
📝 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'. Vulnerable lines: 90, 112 🔎 Evidence: 💥 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:
🔧 Remediation:
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: Secure code: Additional recommendations:
|
| 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 readsdetailsand 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: Thedetailsfield is typedunknownand 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?: stringanddetails?: unknownfields, 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 ofcode. EVIDENCE NOT FOUND: No console pane code was provided showing branching logic that trustscodeequality without validatingdetailsshape, 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 RuntimeManagerTaskResponsecasts 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 stripscode/detailsbefore 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 OffscreenRequestTaskResponsecast → 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.
Details
🛡️ Threat Model & Affect Analysis — PR #171
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | feat/relay-error-code → main |
| 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:
- Attacker-controlled web page calls chrome.runtime.sendMessage with RUNTIME_REQUEST_TASK (EP-001), triggering handleRequestTask in background.ts.
- 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)))).
- 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.
- 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. - 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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:
- 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.
- background.ts's handleRequestTask blindly casts the untyped response with
as OffscreenRequestTaskResponse(line ~1848) without runtime validation (no zod or similar schema check). - Downstream code accesses
res.result.payloadDigest(line ~1851) orres.ok/res.errorassuming the cast type is accurate; if the actual object lacks these fields, this throws a runtime TypeError inside the async handler. - 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).
- 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:
- 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.
- relayFailure calls jsonSafe(value) on this details field (relay-failure.ts).
- 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 === 0check. - The details are silently dropped with no logging, telemetry, or diagnostic record of the original malformed payload.
- 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.
- 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:
- 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.
- 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. - 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).
- 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:
- 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.
- 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.
- 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.
- 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).
- 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:
- handleRequestTask observes
res.result.payloadDigestfrom a consentRequired outcome and callsconsentReplays.recordConsentRequired(key, digest)(background.ts, line ~1851). - 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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:
- 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
detailsshape, e.g.{ personaDids: '<script>...' }or deeply nested/oversized objects, or objects with keys like__proto__. - 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.
- RelayTaskFailure.details (bridge-protocol.ts) carries this unvalidated value to the console via chrome.runtime.sendMessage.
- 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.
- If any console pane renders
details.personaDidsor 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. - Alternatively, if a pane does
Object.assign(target, details)or similar merge patterns without key filtering, a crafted__proto__orconstructor.prototypekey 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:
- A malicious or compromised agent/VTA backend returns a trust-task-error with an extremely large or deeply nested
detailsobject (e.g. megabytes of JSON or thousands of nesting levels). - jsonSafe performs
JSON.parse(JSON.stringify(value))on this payload (relay-failure.ts) without any size or depth guard. - 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.
- 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.
- 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:
- The agent emits a new or uncommon SPEC §8.5 extended error code not yet recognized by coerceTrustTaskCode's mapping table.
- coerceTrustTaskCode buckets this into the generic
e.p.msg.bad_requestVtaErrorCode as documented in relay-failure.ts's comments. - 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).
- 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
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
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
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
📊 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 | |
|---|---|
| 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
detailsfrom 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)
Why
packages/corethrows a typedVtaClientErrorcarrying a stablecodeand structureddetails. Every offscreen/background message handler collapsed it to a string:and
interpretOutcomethen re-wrapped that string in a freshError. By the time a console pane saw a failure, the code and the details were gone and only prose remained.That directly contradicts R3.7 — match 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/deleterefuses while personas are still bound to the profile, with an extended code and adetails.personaDidsnaming them. The console could render neither the reason nor the list.What a pane can now do that it could not
Catch
RelayTaskErrorand branch:It can also tell "the VTA refused" from "the VTA never answered" (
e.client.timeout) without reading a sentence.What changed
bridge-protocol.ts— newRelayTaskFailure: an optionalcodeanddetailsbeside the humanerrorstring, which stays REQUIRED. Most failures have no code worth switching on and prose is all a pane has.RuntimeManagerTaskResponseand the newOffscreenRequestTaskResponseuse it.relay-failure.ts(new) — the one place a rejection becomes a reply. Three decisions live there, each documented against the failure it prevents:VtaErrorCodethe client coerced it to.coerceTrustTaskCodefunnels every extended code it does not recognise intoe.p.msg.bad_request, and its own doc comment says a caller that needs the meaning must read the raw code offdetails. This is that caller.VtaClientError.detailsarrives in two shapes — the wholetrust-task-errorpayload (parseTrustTaskReply) or the server'serror.detailsdirectly (errorFromBody). Both are handled; reading one and assuming the other yieldsundefinedfor everything, which reads as "the agent sent no code" rather than as a bug.detailsis round-tripped through JSON, becausechrome.runtime.sendMessageserializes: anErrorinstance 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— theOFFSCREEN_REQUEST_TASKbranch usesrelayFailure. Only that branch: the others answer wallet UI that renders prose and nothing else.carrier.ts—interpretOutcomethrows the newRelayTaskErrorinstead of a bareError, following theConsentRequiredErrorprecedent in that file. Message text is byte-identical, so anything rendering.messageis unaffected.carrier.tsstays free of relative imports.background.ts—handleManagerTaskpasses the reply through whole; the dispatchcatchfor that branch usesrelayFailuretoo.What deliberately did NOT change
ConsentRequiredError. Untouched, and a test asserts aconsentRequiredoutcome still throws it and notRelayTaskError— reclassifying the ceremony as a failure would render it as a red string at the moment the human was supposed to act.RUNTIME_REQUEST_TASK/content.ts). One offscreen handler answers both relays, so the narrowing is a deliberate act:handleRequestTasknow 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 calledrequestTaskand caught the error. Written as a reconstruction because a cast narrows the type and copies the object whole.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.parseTrustTaskReplyover a realtrust-task-error/0.3document, not by hand-rolling aVtaClientError. A stub built to match whatrelayFailurereads would agree with it by construction and prove nothing. One test asserts the premise — that core nests the agent's code insidedetailsand leaveserr.codeas the coerced bucket — so the rest cannot pass on the fallback path.structuredClone.Errorthrow too.e.client.timeoutkeeps its own code; anerrorFromBodyrefusal keeps details from the other depth.Error(and a cyclic object) handed in as details is dropped, and dropping it costs neither the code nor the sentence.ConsentRequiredErrorunchanged.manager-surface.test.mts, that the page reply shape stays prose-only and that the console's carriesRelayTaskFailure.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.Assert*step from.github/workflows/ci.ymlrun locally against the realdist/: single MV3 worker bundle with no dynamicimport(); 17 admin URIs confined tomanager.js; 3 seeds URIs absent everywhere; 10 holder-scoped persona URIs confined tomanager.jsand still present there; console a single chunk; Web Store zip uploadable (nokey, version matches, no staticcontent_scripts, nocookies, nochrome.cookies).