feat(transports): report the transport actually in use, and why one is not - #143
Conversation
…s not Three linked defects, found while diagnosing a wallet whose mediator refused its origin. The wallet had silently fallen back to REST, its inbox was dark, and nothing in the UI said so — the status line was naming TSP the whole time. Advertisement is not availability. `buildVtaSession` skips a channel whose mediator it cannot reach and falls through, but `activeTransport` derived its answer from the stored connection alone, so the UI named transports that had never carried a byte. It now takes a `TransportHealth` recorded by `buildVtaSession` at the two places that actually decide. REST records `unknown` rather than `up`: a RestChannel is built from a URL without contacting anything, so construction is not evidence — but `unknown` is not a failure and never removes REST from selection. With nothing observed the answer is unchanged; only the label moves, from "Transport in use" to "Transport expected". A CORS refusal is unreadable, so it is inferred. Chrome gives JS a bare `TypeError: Failed to fetch` for a policy refusal and a dead host alike; the reason goes to the devtools console and nowhere an extension can read. `transport-diagnosis.ts` infers it from one bit: a request that fails at the network layer against a host that answers an opaque `no-cors` probe was refused by policy, not by the network. Errors are discriminated structurally (`TypeError`, `DOMException.name`), never on message text (R3.7). The remediation names the mediator's own config key and deliberately never suggests a host permission, which would exempt the REST handshake but not the WebSocket upgrade the mediator checks server-side — a half-fix sends people down the wrong path. The inbox has its own row. "Connected" and "can be reached" are different states, and only the second decides whether an approval request ever arrives (R7.2). A wallet that has fallen back to REST looks entirely healthy on the operating row while nothing can be pushed to it. Adds a read-only connection self-test whose output is plain text meant to be pasted to whoever runs the failing service. It exists because curl cannot reproduce this class of fault: a terminal sends no Origin header, so the endpoint answers perfectly and the operator concludes nothing is wrong. The wallet is the only place the question can be asked truthfully. Also corrects the claim in host-permissions.ts that the mediator is "not subject to CORS". Its WebSocket is not; the auth handshake that must precede it is two ordinary fetches, and that is what fails. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review1 AI-confirmed issue, 2 findings need a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #143
🗺️ Scan CoverageModules scanned: 2 · with findings: 1 · files: 16 · findings: 3
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (1)🟡 Missing sender validation on RUNTIME_RUN_DIAGNOSTICS / RUNTIME_TRANSPORT_HEALTH chrome.runtime message listeners
🧠 AI Triage:
Summary: The background script's chrome.runtime.onMessage handler dispatches RUNTIME_RUN_DIAGNOSTICS and RUNTIME_TRANSPORT_HEALTH purely on message.type, without checking sender.id, letting any context with runtime messaging access to this extension trigger diagnostics against an arbitrary vtaDid. 📝 Description: An attacker with runtime-messaging access to this extension can force it to bring up its offscreen document and run network diagnostics against an attacker-chosen vtaDid, potentially probing internal network reachability or consuming resources repeatedly. 🧪 Proof of Concept: No check of Vulnerable lines: 1959, 1974 🔎 Evidence: 💥 Impact: An attacker with runtime-messaging access to this extension can force it to bring up its offscreen document and run network diagnostics against an attacker-chosen vtaDid, potentially probing internal network reachability or consuming resources repeatedly. Confidentiality: Low — can trigger network probes revealing reachability/timing info about mediator endpoints · Integrity: None directly · Availability: Low — repeated invocation can force resource consumption via offscreen document lifecycle 🧭 Reachability:
⚖️ Triage Factors:
Attack scenario: A sender without proper isolation (another extension or compromised context) sends a raw chrome.runtime message to trigger diagnostics against an attacker-chosen vtaDid, bypassing any expectation that only the wallet's own popup can invoke this. 🔧 Remediation:
Adding a sender.id === chrome.runtime.id check (Manifest V3 standard hardening pattern) ensures the message genuinely originates from this extension's own popup/pages, not from another installed extension or an unexpected context. Combined with basic input-shape validation of vtaDid, this closes the unauthenticated-trigger gap. Vulnerable code: Secure code: Additional recommendations:
|
| Field | Detail |
|---|---|
| Severity | LOW |
| Location | packages/extension/src/offscreen.ts:605 |
| Finding ID | github_pr-91ecd5db5920 |
| OWASP | A01:2021 - Broken Access Control |
| CVSS 4.0 | 3.5 |
| Exploit Maturity | conceptual |
| Detection Source | mcp_semgrep |
Summary: Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co — 3 occurrence(s): offscreen.ts:605, offscreen.ts:1172, offscreen.ts:1181
📝 Description:
Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co
🌱 Root Cause: Unsafe Formatstring
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Priority: Short-term
Unsafe Formatstring: Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 85%
- AI Validation Evidence: EVIDENCE FOUND: The finding points to packages/extension/src/offscreen.ts line 605 but the evidence.code_snippet field is empty and offscreen.ts source was not included in source_files, so no actual concatenation/format-string code could be inspected. EVIDENCE NOT FOUND: No quoted line of code showing util.format/console.log with attacker-controlled input; the file offscreen.ts content is absent from provided source_files entirely. CHANGED VS PRE-EXISTING: offscreen.ts is referenced throughout t
- 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.
⚪ Unvalidated DID-resolved endpoint used as SSRF-style fetch target in diagnostics self-test (offscreen.ts / transport-diagnosis.ts) — SUSPECTED, source not fully provided
| Field | Detail |
|---|---|
| Severity | INFORMATIONAL |
| Location | packages/extension/src/background.ts:1107 |
| Finding ID | github_pr-507c464f87d2 |
| CWE | CWE-918, CWE-441 |
| OWASP | A10:2021 - Server-Side Request Forgery (SSRF) |
| MITRE ATT&CK | T1090 (Proxy) analog for browser-mediated SSRF |
| CAPEC | CAPEC-664, CAPEC-153 |
| DREAD | 4.6 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | conceptual |
| Detection Source | skill_scan |
Summary: The diagnostics self-test resolves a caller-supplied vtaDid and (per CLAUDE.md's documented behavior, not directly verified in the provided offscreen.ts/transport-diagnosis.ts source) performs live network fetches to the DID-resolved host without apparent allow-listing, creating a classic SSRF pattern mediated by DID resolution.
📝 Description:
The extension's privileged fetch context can be induced to issue outbound requests to attacker-chosen or internal infrastructure, functioning as an SSRF proxy and potentially disclosing internal network topology via timing/error behavior differences.
🧪 Proof of Concept:
req.vtaDid flows unchecked into the offscreen document, which per documentation resolves it and performs live network probes; no validation of the DID or its resolved endpoints is visible in the provided code.
async function handleRunDiagnostics(
req: RuntimeRunDiagnosticsRequest,
): Promise<RuntimeRunDiagnosticsResponse> {
await ensureOffscreenDocument();
return (await chrome.runtime.sendMessage({
target: OFFSCREEN_TARGET,
type: OFFSCREEN_RUN_DIAGNOSTICS,
vtaDid: req.vtaDid,
})) as RuntimeRunDiagnosticsResponse;
}
Vulnerable lines: 1107, 1131
🔎 Evidence: packages/extension/src/background.ts:1107
async function handleRunDiagnostics(
req: RuntimeRunDiagnosticsRequest,
): Promise<RuntimeRunDiagnosticsResponse> {
await ensureOffscreenDocument();
return (await chrome.runtime.sendMessage({
target: OFFSCREEN_TARGET,
type: OFFSCREEN_RUN_DIAGNOSTICS,
vtaDid: req.vtaDid,
})) as RuntimeRunDiagnosticsResponse;
}
💥 Impact:
The extension's privileged fetch context can be induced to issue outbound requests to attacker-chosen or internal infrastructure, functioning as an SSRF proxy and potentially disclosing internal network topology via timing/error behavior differences.
Confidentiality: Low-Medium — could reveal internal network topology/reachability via timing/error differences · Integrity: None directly · Availability: Low
🧭 Reachability:
- Network exposure: public
- Auth barrier: none
- Attack path: EP-001 → background.ts handleRunDiagnostics(req.vtaDid) → OFFSCREEN_RUN_DIAGNOSTICS → offscreen.ts runDiagnostics() [NOT DIRECTLY VERIFIED] → DID resolution → fetch(resolvedEndpoint)
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | medium |
| Business impact | medium |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: An attacker who can influence a VTA's DID document resolution (or induce a victim to add a malicious VTA connection) causes the wallet's diagnostics feature to issue live network probes from the extension's privileged origin toward attacker-controlled or internal infrastructure.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Validate resolved DID service endpoints against scheme and private/internal IP-range allowlists before issuing any diagnostic fetch, preventing the extension's privileged fetch context from being used as an SSRF oracle against internal or attacker-controlled infrastructure.
Vulnerable code:
// (inferred, offscreen.ts not fully provided) resolveDid(vtaDid).then(doc => fetch(doc.service.authEndpoint + '/challenge', ...))
Secure code:
const ALLOWED_SCHEMES = ['https:'];
function isPrivateOrLocal(hostname: string): boolean {
return /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|localhost$|::1$)/.test(hostname);
}
function assertSafeEndpoint(url: URL) {
if (!ALLOWED_SCHEMES.includes(url.protocol)) throw new Error('unsupported scheme');
if (isPrivateOrLocal(url.hostname)) throw new Error('refusing to probe private/internal host');
}
// before any fetch to a DID-resolved endpoint:
assertSafeEndpoint(new URL(resolvedEndpoint));
Additional recommendations:
- Require explicit user confirmation showing the resolved host before running diagnostics against a newly added VTA
- Rate-limit and timeout all diagnostic fetches aggressively
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND: handleRunDiagnostics in background.ts forwards req.vtaDid unchanged into chrome.runtime.sendMessage to the offscreen document: 'return (await chrome.runtime.sendMessage({ target: OFFSCREEN_TARGET, type: OFFSCREEN_RUN_DIAGNOSTICS, vtaDid: req.vtaDid }))'. This confirms taint propagation from the RUNTIME_RUN_DIAGNOSTICS message into the offscreen relay. EVIDENCE NOT FOUND: The actual DID resolution and fetch logic (offscreen.ts's runDiagnostics, checkCorsReachable, authenticateToMe
- 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 #143
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | feat/transport-diagnostics → main |
| Generated | 2026-09-05 |
ℹ️ 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 evidence-based transport health tracking (TransportHealth: up/down/unknown) recorded at the point buildVtaSession actually decides, replacing UI logic that previously derived 'active transport' from advertised DID capabilities alone. Adds a new read-only connection self-test (RUNTIME_RUN_DIAGNOSTICS) that structurally infers CORS refusal on the mediator auth handshake — a failure mode that silently degrades both TSP and DIDComm to REST-only and leaves the wallet's inbox unreachable — and surfaces it via a new DiagnosticsPanel and Inbox status row in the popup UI.
Diff: +245 / -32 lines
Types: feature, security, docs, test
📁 File Classifications
packages/extension/src/bridge-protocol.ts
- Type: security
packages/extension/src/background.ts
- Type: security
packages/extension/src/app-shell.tsx
- Type: security
packages/extension/src/diagnostics-panel.tsx
- Type: security
packages/extension/src/transport-diagnosis.ts
- Type: security
packages/extension/src/transports.ts
- Type: security
🛡️ STRIDE Threat Model
Identified Threats (12)
🟡 STRIDE-1: Unauthenticated Runtime Message Spoofing in RUNTIME_RUN_DIAGNOSTICS Listener
| Field | Detail |
|---|---|
| Category | Spoofing, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.3 CVSS:4.0/AV:L/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-346,CWE-862 |
| CAPEC | CAPEC-148,CAPEC-668 |
| OWASP | A01:2021 - Broken Access Control |
Description: chrome.runtime.onMessage listener for RUNTIME_RUN_DIAGNOSTICS in background.ts allows any extension-context sender to trigger diagnostics due to missing sender.id validation, resulting in unauthorized invocation of the offscreen document and mediator probing.
Evidence: packages/extension/src/background.ts:~1959-1966
if ((message as { type?: string })?.type === RUNTIME_RUN_DIAGNOSTICS) {
handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest)
.then(sendResponse)
Attack Scenario:
- Attacker identifies that chrome.runtime.onMessage.addListener in background.ts (message as {type?:string})?.type === RUNTIME_RUN_DIAGNOSTICS branch does not check sender.id or sender.origin.
- A malicious or compromised content script / another extension with externally_connectable access sends a message {type: RUNTIME_RUN_DIAGNOSTICS, vtaDid: }.
- handleRunDiagnostics(req) is invoked, calling ensureOffscreenDocument() and forwarding OFFSCREEN_RUN_DIAGNOSTICS to the offscreen document with attacker-controlled vtaDid.
- The offscreen document resolves the attacker-supplied vtaDid and performs live network probes (checkCorsReachable, authenticateToMediator challenge POST) against endpoints derived from that DID.
- Repeated invocation with attacker-chosen vtaDid values can be used to fingerprint internal network reachability (SSRF-adjacent) or to keep the offscreen document alive, consuming resources.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: background->offscreen diagnostics relay
Preconditions: Attacker can send a chrome.runtime message to the extension's background script (e.g. via another installed extension, a compromised tab with access to chrome.runtime, or if externally_connectable is misconfigured), No sender validation present in the onMessage listener
Existing Controls: Manifest V3 default runtime.onMessage restricts external pages unless externally_connectable is set (not shown, assumed default)
Recommended Mitigations: Validate sender.id === chrome.runtime.id and reject messages from unexpected senders • Validate vtaDid format/allowlist before forwarding to offscreen • Rate-limit RUNTIME_RUN_DIAGNOSTICS invocations per popup session
🟠 STRIDE-2: SSRF via Attacker-Controlled vtaDid in Diagnostics Self-Test
| Field | Detail |
|---|---|
| Category | Tampering, Information Disclosure, Denial of Service |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:L/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-918,CWE-441 |
| CAPEC | CAPEC-664,CAPEC-153 |
| OWASP | A10:2021 - Server-Side Request Forgery |
Description: OFFSCREEN_RUN_DIAGNOSTICS in offscreen.ts allows Server-Side-Request-Forgery-like abuse due to unvalidated resolution and fetch of endpoints derived from an attacker-influenced vtaDid, resulting in forced outbound requests from the extension's origin to arbitrary hosts.
Evidence: packages/extension/src/background.ts:~1107-1132
async function handleRunDiagnostics(req: RuntimeRunDiagnosticsRequest): Promise<RuntimeRunDiagnosticsResponse> {
await ensureOffscreenDocument();
return (await chrome.runtime.sendMessage({... vtaDid: req.vtaDid }))
Attack Scenario:
- Attacker crafts or registers a malicious did:webvh / did:web document that resolves to attacker-controlled authEndpoint and mediator URLs.
- Attacker (via UI trick, malicious RP page, or a spoofed connection object) causes the wallet to add this VTA as an active connection, or directly triggers RUNTIME_RUN_DIAGNOSTICS with the malicious vtaDid.
- runDiagnostics (offscreen.ts, referenced in CLAUDE.md) resolves the DID and performs checkCorsReachable GET and authenticateToMediator POST {authEndpoint}/challenge against attacker-controlled infrastructure.
- The extension's origin-bearing fetch is used as a probing oracle: attacker infrastructure receives requests originating from the wallet's extension origin and can observe timing/response, or the wallet can be induced to hit internal/local network addresses reachable from the browser.
- Diagnostic report content (endpoint names, reachability, timing) is returned to the popup UI and can be exfiltrated if combined with another disclosure vector.
🔎 Threat Clue: Derived from COMP-002, COMP-007 via EP-005, EP-006
- Data Flows: offscreen->mediator diagnostic probe
Preconditions: Attacker controls or influences DID document resolution for a VTA the wallet is induced to diagnose, No allowlist/validation of resolved authEndpoint/mediator host before fetch
Existing Controls: Requests are same-origin restricted by browser fetch/CORS semantics for response reading (no-cors probe limits response visibility) • runDiagnostics is read-only per CLAUDE.md notes
Recommended Mitigations: Validate resolved DID service endpoints against an allowlist or scheme/host restrictions before probing • Block private/internal IP ranges (RFC1918, loopback, link-local) in diagnostic fetch targets • Require explicit user confirmation showing the resolved host before running diagnostics against a newly-added VTA
🟠 STRIDE-3: Transport Downgrade via Silent Fallback to REST in buildVtaSession
| Field | Detail |
|---|---|
| Category | Tampering, Information Disclosure |
| Severity | High |
| Likelihood | Likely |
| CVSS | 7.4 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-757,CWE-300 |
| CAPEC | CAPEC-594,CAPEC-storage-attack-593 |
| OWASP | A02:2021 - Cryptographic Failures |
Description: buildVtaSession in transports.ts allows a silent transport downgrade due to skipping unreachable TSP/DIDComm channels and falling through to REST without cryptographic channel binding, resulting in loss of end-to-end transport guarantees while the DID document still advertises stronger channels.
Evidence: CLAUDE.md:1-20
A VTA's DID document says what it offers. buildVtaSession skips a channel whose mediator it cannot reach and falls through to the next, so a wallet routinely advertises TSP, DIDComm and REST while every byte goes over REST.
Attack Scenario:
- An on-path or malicious mediator operator (or an attacker who can block UDP/WS to the legitimate mediator, e.g. via network MITM or DNS manipulation) makes TSP/DIDComm mediator handshakes fail while leaving REST endpoints reachable.
- buildVtaSession (transports.ts) attempts TSP first, catches the failure, attempts DIDComm, catches that failure too, and falls through to constructing a RestChannel from the DID document's REST service endpoint.
- Because REST session state is recorded as 'unknown' rather than actively verified, the wallet proceeds to route sensitive VC/VP exchange or approval traffic over REST.
- If REST lacks the same message-level authenticated encryption as TSP/DIDComm (dependent on RA-# 'VC/VP payloads'), the attacker who forced the downgrade gains a weaker channel to intercept or tamper with the exchange.
- Because activeTransport() previously derived UI state from stored connection alone (pre-fix) and even post-fix marks REST 'unknown' (not verified-down), the user may not receive a strong enough warning that a downgrade occurred.
🔎 Threat Clue: Derived from COMP-002, COMP-006 via EP-008
- Data Flows: mediator session negotiation
Preconditions: Attacker can selectively block/degrade WebSocket connectivity or the mediator auth handshake without blocking plain HTTP REST, VTA DID document advertises multiple transports with different security postures
Existing Controls: TransportHealth up/down/unknown states now recorded (post-fix) and surfaced via UI warn indicator • CLAUDE.md documents explicit design intent to avoid overconfident 'up' status for constructed-but-unverified channels
Recommended Mitigations: Require explicit user confirmation before falling back to a lower-assurance transport for sensitive operations • Cryptographically bind VC/VP payload confidentiality/integrity at the message layer independent of transport (so REST fallback does not weaken protection) • Surface a persistent, high-visibility (not just a Pill warn) notification when the active transport differs from the DID-advertised preferred transport
🟡 STRIDE-4: CORS Refusal Masking Enables Silent Inbox Blackout
| Field | Detail |
|---|---|
| Category | Denial of Service, Information Disclosure |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 6.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-390,CWE-703 |
| CAPEC | CAPEC-599,CAPEC-125 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: authenticateToMediator's cross-origin POST to {authEndpoint}/challenge in the mediator handshake allows denial of the wallet's inbox due to reliance on opaque browser TypeError messages that are indistinguishable between CORS refusal, DNS failure, and dead host, resulting in undiagnosable loss of inbound approval-request delivery.
Evidence: CLAUDE.md:20-40
authenticateToMediator POSTs to {authEndpoint}/challenge before any socket exists, so a mediator whose [security] cors_allow_origin omits this extension's origin takes out TSP and DIDComm together
Attack Scenario:
- A mediator operator (malicious, misconfigured, or coerced by an attacker who compromises mediator config) fails to include the extension's origin in [security] cors_allow_origin.
- The wallet's authenticateToMediator POSTs to {authEndpoint}/challenge as a cross-origin fetch; the browser blocks the response and raises a generic TypeError: Failed to fetch, indistinguishable from a dead host or DNS failure.
- Both TSP and DIDComm transports fail because they share this same auth handshake, silently leaving REST as the only carrier.
- Because the inbox (isInbox session) depends on the same mediator handshake, no inbound approval/consent requests can reach the wallet, and the user sees only a generic 'Offline' pill without root cause.
- An attacker who wants to suppress a victim's ability to receive step-up/consent prompts (e.g. during a social-engineering attack that requires the victim NOT to notice an approval request) could intentionally target/compromise the mediator's CORS config or block the specific origin to silently disable the inbox path, while REST continues to look 'fine' for outbound-looking status.
🔎 Threat Clue: Derived from COMP-002, COMP-006 via EP-005
- Data Flows: mediator authentication handshake
Preconditions: Attacker can influence or compromise the mediator's CORS configuration, or perform network-level interference distinguishing between origins, User relies on wallet UI status rather than independently verifying mediator configuration
Existing Controls: transport-diagnosis.ts infers CORS refusal via structural TypeError/TimeoutError discrimination (no message-text parsing) • diagnostics-panel.tsx surfaces a self-test report with remediation text aimed at the mediator operator • Inbox status separated from generic 'connected' status in app-shell.tsx (R7.2 fix)
Recommended Mitigations: Add automated periodic diagnostics (not just on-demand) with alerting when inbox transitions from live to offline • Provide a way to detect malicious CORS-origin removal by an attacker with mediator config access (out of extension's control, but log/alert on this transition) • Consider redundant mediator configuration to avoid single point of CORS-based failure for inbox delivery
🟡 STRIDE-5: Unauthenticated Cross-Context Message Trust in RUNTIME_TRANSPORT_HEALTH Response Handling
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 CVSS:4.0/AV:L/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-345,CWE-20 |
| CAPEC | CAPEC-148 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: handleTransportHealth in background.ts allows tampered health status to reach the popup UI due to unauthenticated chrome.runtime.sendMessage response trust between background and offscreen contexts, resulting in a spoofed 'healthy' transport state being displayed to the user.
Evidence: packages/extension/src/background.ts:~1114-1123
async function handleTransportHealth(): Promise<RuntimeTransportHealthResponse> {
try {
const res = (await chrome.runtime.sendMessage({ target: OFFSCREEN_TARGET, type: OFFSCREEN_TRANSPORT_HEALTH }))
Attack Scenario:
- If an attacker can inject or replace the offscreen document (e.g. via a compromised extension update, supply-chain compromise of a dependency loaded by offscreen.ts, or a bug allowing arbitrary script injection into the offscreen context), they control the response to OFFSCREEN_TRANSPORT_HEALTH.
- handleTransportHealth() in background.ts accepts the offscreen response verbatim: res ?? { ok: true, result: { byVta: {}, sessions: [] } } with no integrity check on the shape or truthfulness of TransportHealthResult.
- The attacker returns a crafted TransportHealthResult marking all transports 'up' and the inbox session 'live' even though the mediator handshake actually failed.
- app-shell.tsx renders this as Pill tone='ok' Live, giving the victim false confidence that inbound approval requests can be delivered.
- This false confidence can be leveraged to make a victim believe an out-of-band approval channel is active while an attacker performs a separate action expecting no real-time consent challenge to interrupt them.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-002, EP-004
- Data Flows: background<->offscreen transport health relay
Preconditions: Attacker has already achieved code execution within the offscreen document or background context (high bar) OR a future refactor introduces external message acceptance without target validation
Existing Controls: Offscreen documents in MV3 are only creatable/controlled by the extension's own background script; chrome.runtime messaging within the extension is not directly reachable by web content
Recommended Mitigations: Add schema/shape validation (e.g. zod) on TransportHealthResult before trusting it in handleTransportHealth • Consider a signed/nonce-based handshake between background and offscreen contexts for defense-in-depth • Log discrepancies between claimed transport health and independently observed diagnostics results
🔵 STRIDE-6: Missing Rate Limiting on ensureOffscreenDocument Triggers Resource Exhaustion
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 4.0 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-770 |
| CAPEC | CAPEC-125 |
| OWASP | A04:2021 - Insecure Design |
Description: RUNTIME_RUN_DIAGNOSTICS handler in background.ts allows repeated offscreen document creation/network probing due to absence of debounce or rate limiting on user- or script-triggered diagnostics runs, resulting in excessive outbound network requests and battery/CPU exhaustion.
Evidence: packages/extension/src/background.ts:~1107-1113
async function handleRunDiagnostics(req: RuntimeRunDiagnosticsRequest): Promise<RuntimeRunDiagnosticsResponse> {
await ensureOffscreenDocument();
Attack Scenario:
- A malicious page or compromised extension repeatedly sends RUNTIME_RUN_DIAGNOSTICS messages in a tight loop (bounded only by message-passing throughput).
- Each call triggers handleRunDiagnostics -> ensureOffscreenDocument() -> OFFSCREEN_RUN_DIAGNOSTICS, which performs checkCorsReachable and authenticateToMediator network calls per run.
- No visible throttling/debounce exists in the shown code path; the popup's 'Run checks' button only prevents concurrent runs via local busy state, which does not protect the background listener from direct message injection bypassing the UI.
- Repeated invocation drains device battery/network resources and may also generate excessive/duplicate authentication challenge requests against the mediator, indirectly resembling a distributed nuisance against the mediator's /challenge endpoint.
- Sustained abuse degrades wallet responsiveness and could trip mediator-side rate limiting for the legitimate user.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001
- Data Flows: popup->background->offscreen diagnostics trigger
Preconditions: Attacker (another extension or malicious page context, if reachable) can send repeated chrome.runtime messages bypassing the popup UI's busy-state guard
Existing Controls: Popup's local busy state (setBusy) throttles UI-triggered runs • ensureOffscreenDocument likely dedupes concurrent document creation attempts (MV3 offscreen API behavior)
Recommended Mitigations: Add server-side (background script) rate limiting / cooldown per vtaDid for RUNTIME_RUN_DIAGNOSTICS independent of UI state • Add sender validation to reject non-popup senders for this message type
🔵 STRIDE-7: Diagnostics Report Clipboard Exfiltration of Mediator Topology
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.5 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-200 |
| CAPEC | CAPEC-651 |
| OWASP | A01:2021 - Broken Access Control |
Description: copy() clipboard handler in diagnostics-panel.tsx allows sensitive infrastructure details to be exposed via the OS clipboard due to writing the full formatted diagnostics report (including extensionOrigin, mediator endpoints and remediation detail) without redaction, resulting in potential information disclosure if the clipboard is subsequently synced or accessed by other clipboard-reading software.
Evidence: packages/extension/src/diagnostics-panel.tsx:~60-70
async function copy() {
if (!report) return;
try {
await navigator.clipboard.writeText(formatReport(report));
Attack Scenario:
- User runs the connection self-test and clicks Copy in DiagnosticsPanel (diagnostics-panel.tsx).
- navigator.clipboard.writeText(formatReport(report)) places the full DiagnosticsReport — including extensionOrigin and mediator/authEndpoint hostnames plus remediation text — onto the system clipboard.
- Any other application, browser extension with clipboard-read permission, or OS-level clipboard sync service (e.g. cross-device clipboard sharing) can read this clipboard content.
- Mediator/agent infrastructure hostnames and the wallet's extension origin are disclosed to whatever reads the clipboard next, aiding reconnaissance for a follow-on targeted attack against the user's mediator or wallet install.
- Because the user is instructed to paste this to 'whoever operates the service' (per CLAUDE.md intent), the report may also end up in a support ticket, chat log, or email — a persistent, less-controlled disclosure surface beyond the extension's control.
🔎 Threat Clue: Derived from COMP-004 via EP-007
- Data Flows: diagnostics report clipboard export
Preconditions: User invokes the copy action, Another application/extension/service has OS clipboard read access
Existing Controls: Clipboard write failure is caught and silently ignored (does not create additional error surface) • Report contents are meant to be shared with an operator by design, per CLAUDE.md rationale
Recommended Mitigations: Warn the user before copy that the report contains infrastructure hostnames intended for the mediator operator • Avoid including highly sensitive identifiers (e.g. full vtaDid) unless necessary for remediation
🟡 STRIDE-8: Unvalidated Type Assertions on Runtime Messages Enable Malformed Payload Injection
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-20 |
| CAPEC | CAPEC-153 |
| OWASP | A03:2021 - Injection |
Description: chrome.runtime.onMessage listener in background.ts allows malformed/malicious payload propagation due to blind 'as RuntimeRunDiagnosticsRequest' / 'as RuntimeTransportHealthRequest' TypeScript type assertions without runtime schema validation, resulting in unvalidated vtaDid and other fields flowing into offscreen document handlers and downstream network calls.
Evidence: packages/extension/src/background.ts:~1959
handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest)
Attack Scenario:
- TypeScript type assertions (message as RuntimeRunDiagnosticsRequest) are compile-time only and provide zero runtime guarantee about the actual shape of
message. - An attacker-controlled sender sends {type: RUNTIME_RUN_DIAGNOSTICS, vtaDid: {malicious: 'object'}} or vtaDid as an oversized string, an object with prototype-polluting keys, or a URL-like string designed to be misinterpreted downstream.
- handleRunDiagnostics forwards req.vtaDid directly into the OFFSCREEN_RUN_DIAGNOSTICS message without validation.
- Depending on how offscreen.ts's runDiagnostics resolves vtaDid (DID resolution, string concatenation into URLs), a non-string or malformed vtaDid could cause unexpected behavior: exceptions leaking stack traces, resolver misbehavior, or (if concatenated into a URL/log) injection-style issues.
- Because DiagnosticsReport.vtaDid is echoed back into the report and displayed/copied by the user, a crafted vtaDid could also be used for UI spoofing/text injection in the copied report (e.g., embedding CRLF or terminal-escape sequences if the report is ever piped to a terminal by the operator).
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-003
- Data Flows: popup->background->offscreen message payloads
Preconditions: Attacker can inject a message of unexpected shape into chrome.runtime.onMessage, Downstream offscreen handler does not itself re-validate vtaDid type/format
Existing Controls: TypeScript static typing at build time (does not provide runtime protection)
Recommended Mitigations: Add runtime schema validation (zod/io-ts) for every RUNTIME_* and OFFSCREEN_* message before use • Sanitize/validate vtaDid format (DID syntax) before passing to resolver or including in reports • Strip/escape control characters from any user-facing or clipboard-bound report fields
🔵 STRIDE-9: Repudiation of Diagnostic Runs Due to Absent Audit Logging
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 2.8 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-778 |
| CAPEC | CAPEC-93 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: handleRunDiagnostics in background.ts allows undetectable repeated probing of mediator infrastructure due to lack of any persistent audit log of who/when/how often diagnostics were run, resulting in inability to attribute or investigate abuse of the diagnostics feature as an SSRF/DoS primitive.
Evidence: packages/extension/src/background.ts:~1107-1132
async function handleRunDiagnostics(req: RuntimeRunDiagnosticsRequest): Promise<RuntimeRunDiagnosticsResponse> {
Attack Scenario:
- No logging mechanism is visible in handleRunDiagnostics or handleTransportHealth beyond ephemeral in-memory state and console (implicit).
- An attacker abusing RUNTIME_RUN_DIAGNOSTICS for SSRF-style probing (see STRIDE-2) or DoS (STRIDE-6) leaves no durable record within the extension.
- If a mediator operator later reports anomalous probing traffic apparently from the extension's origin, the wallet vendor/user has no local evidence to confirm, deny, or investigate the timeline of diagnostics invocations.
- This absence of non-repudiation controls compounds the impact of STRIDE-2 and STRIDE-6 by removing forensic traceability.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: diagnostics invocation (no persistence)
Preconditions: An abuse event (SSRF/DoS via diagnostics) has already occurred, Investigation requires historical evidence of diagnostics invocation
Existing Controls: DiagnosticsReport includes generatedAt timestamp visible to the user for a single run (not persisted as an audit trail)
Recommended Mitigations: Persist a local (or optionally sync) audit log of diagnostics runs including timestamp, vtaDid, and initiating context • Add structured logging on background.ts message handlers for security-relevant events
🔵 STRIDE-10: UI Deception via Misleading 'Connecting' State Masking Persistent Failure
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-696 |
| CAPEC | CAPEC-125 |
| OWASP | A04:2021 - Insecure Design |
Description: Inbox status rendering in app-shell.tsx allows a persistently broken inbox to be misrepresented as transient due to the 'connecting' Pill tone lacking a timeout/stuck-state detection, resulting in a user believing recovery is imminent when the mediator handshake is permanently failing.
Evidence: packages/extension/src/app-shell.tsx:~305-325
inbox === "connecting" ? (
<Pill tone="warn">Connecting</Pill>
) : (...)
warn={Boolean(connection) && inbox !== "live" && inbox !== "connecting"}
Attack Scenario:
- Mediator handshake enters a state where it never resolves to 'live' nor cleanly errors to 'closed' (e.g., a slow-loris style mediator or a network condition that causes repeated silent retries).
- inbox state remains 'connecting' indefinitely; app-shell.tsx renders Pill tone='warn' Connecting and note='opening a mediator session' with warn=false (since inbox === 'connecting' is excluded from the warn condition).
- The user, seeing a non-alarming 'Connecting' status rather than a warn-flagged failure, does not investigate further or run the connection self-test.
- An attacker who can induce a permanently-stalling handshake (e.g., a malicious/compromised mediator that accepts the challenge POST but never completes the WS upgrade) can keep a victim's inbox inviscould effectively offline while the UI signals normalcy, delaying detection of a targeted denial-of-inbox attack (e.g., to suppress delivery of a fraud alert or step-up consent request during a separate social-engineering attack).
🔎 Threat Clue: Derived from COMP-004, COMP-006 via EP-008
- Data Flows: inbox session status rendering
Preconditions: Attacker can influence mediator behavior to stall (not fail) the handshake indefinitely, User relies on the Pill tone/warn flag rather than elapsed time
Existing Controls: Distinct inbox row separates 'connected' from 'reachable' semantics (R7.2 intent) • Diagnostics self-test available on demand to override the stuck status view
Recommended Mitigations: Add a timeout for the 'connecting' state after which it is reclassified as 'down'/warn • Surface elapsed time in the 'Connecting' note so a stuck state becomes visually obvious • Encourage periodic automatic diagnostics when 'connecting' persists beyond a threshold
🟡 STRIDE-11: Supply Chain Risk in Offscreen Document Dynamic import() for DID Resolution
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.0 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 | Medium |
| CWE | CWE-1104 |
| CAPEC | CAPEC-437 |
| OWASP | A06:2021 - Vulnerable and Outdated Components |
Description: offscreen.ts allows execution of compromised code paths due to dynamic import()-based loading of DID resolution and mediator session modules (per background.ts comment 'both phases run in the offscreen doc... need import()/DOM'), resulting in expanded blast radius if any transitively-loaded dependency is compromised in a build/publish pipeline.
Evidence: packages/extension/src/background.ts:~1132-1136
// Onboarding (popup-driven): both phases run in the offscreen doc (DID
// resolution + the mediator session need import()/DOM).
Attack Scenario:
- background.ts comments confirm offscreen.ts performs DID resolution and mediator session setup via import()/DOM APIs not available in the service worker.
- If any package in the dependency tree used by these dynamically-imported modules (e.g., a DID resolver library, a DIDComm/TSP transport library) is compromised via a supply-chain attack (typosquatting, compromised maintainer account, malicious transitive dependency update), the malicious code executes inside the offscreen document with access to fetch, DOM, and all bridge-protocol message handlers it can reach.
- Because the offscreen document handles OFFSCREEN_RUN_DIAGNOSTICS, OFFSCREEN_TRANSPORT_HEALTH, and (per file tree) DIDComm login/session logic, compromised code here could exfiltrate mediator credentials, forge TransportHealthResult responses (see STRIDE-5), or tamper with VC/VP exchange.
- This is compounded by the extension's elevated trust boundary — offscreen documents in MV3 run with the extension's full origin and permissions, but their dependency provenance is governed entirely by the npm/build pipeline, outside runtime code review.
🔎 Threat Clue: Derived from COMP-002 via EP-003, EP-004
- Data Flows: offscreen dynamic module loading
Preconditions: A dependency used inside offscreen.ts's dynamically-imported modules is compromised upstream, No subresource integrity / dependency pinning+audit process prevents the compromised version from being bundled
Existing Controls: Presumed use of lockfiles (package-lock/pnpm-lock) limiting silent version drift (not confirmed in provided files) • CLAUDE.md indicates deliberate, reviewed security design intent for this codebase, suggesting some review discipline
Recommended Mitigations: Adopt Software Bill of Materials (SBOM) generation and dependency pinning with checksum verification for all offscreen-loaded packages • Use Subresource Integrity or bundle-time hashing for dynamically imported modules • Run automated SCA (e.g., Snyk/Dependabot) scanning in CI as a standing control (noted as out of scope for this threat model per instructions, but flagged as a governance gap)
🟡 STRIDE-12: Race Condition Between Concurrent buildVtaSession Calls and TransportHealth Recording
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.4 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-362 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: buildVtaSession's two health-recording call sites in transports.ts allow a stale/incorrect TransportHealth entry to persist due to a TOCTOU race when multiple session builds for the same VTA run concurrently (e.g., user reconnect + background poll), resulting in the UI displaying transport status from a superseded connection attempt.
Evidence: CLAUDE.md:9-14
activeTransport (transports.ts) now takes a TransportHealth, recorded by buildVtaSession at the two places it decides — and only there, because that is the only code that knows.
Attack Scenario:
- User triggers a manual reconnect (e.g., via Setup pane) while a background-initiated session rebuild for the same vtaDid is already in flight.
- Both invocations of buildVtaSession race to write TransportHealth entries keyed by vtaDid in the shared byVta map maintained by the offscreen document.
- The slower call's result (e.g., recording TSP as 'down' from a transient network blip) overwrites the faster, more current call's result (e.g., TSP now 'up'), or vice versa, depending on completion order.
- useTransportHealth (popup hook) polls/receives the stale overwritten state via RUNTIME_TRANSPORT_HEALTH, and app-shell.tsx renders an incorrect transport/inbox status to the user without any indication that two builds raced.
- A user acting on the stale 'no usable transport' warning might unnecessarily abandon a working session, or conversely trust a stale 'up' status that no longer reflects the current (now degraded) connection.
🔎 Threat Clue: Derived from COMP-002, COMP-004 via EP-004
- Data Flows: transport health state map writes
Preconditions: Two or more buildVtaSession calls for the same vtaDid execute concurrently, No locking/versioning on the byVta health map writes
Existing Controls: CLAUDE.md documents that health is recorded 'at the two places it decides — and only there' implying a deliberate single-writer design intent, but concurrency between separate invocations is not addressed in the visible documentation
Recommended Mitigations: Add a monotonic sequence/version number per vtaDid session build; discard health writes from a stale generation • Serialize buildVtaSession invocations per vtaDid with a mutex/queue • Include a build/session id in TransportHealthView so the UI can detect and ignore stale updates
🍝 PASTA Threat Model
Application Purpose
A Manifest V3 browser extension implementing a Verifiable Trust Agent (VTA) wallet that resolves DIDs, negotiates multi-transport (TSP/DIDComm/REST) mediator sessions, and now adds transport-health observability and a CORS-aware connection self-test so users and mediator operators can diagnose why the wallet's advertised transports are not actually carrying traffic.
Inherent Risks
- Browser extensions inherently expose a large, semi-trusted message-passing surface between content scripts, background, offscreen, and popup contexts.
- DID-based trust agents depend on externally-operated mediator infrastructure whose CORS/network configuration is outside the wallet's control.
- Multi-transport fallback logic inherently risks silent security-relevant downgrades that are hard to communicate clearly to end users.
Objectives
Risk: Accept residual risk of mediator-side CORS misconfiguration as outside extension control, but ensure it is diagnosable, not hidden; Treat any 'unknown' transport health state as non-authoritative and never equivalent to verified 'up'
Business: Provide a trustworthy self-sovereign identity wallet that reliably informs users which transport actually carries their traffic; Reduce support burden by letting users generate actionable diagnostic reports for mediator operators
Security: Prevent unauthorized or unauthenticated actors from triggering network probes via the diagnostics message surface; Ensure transport downgrade events are visible and cannot be silently misrepresented as full functionality; Prevent supply-chain compromise of dynamically-imported DID/mediator resolution code from escalating into credential or channel compromise
Financial: Avoid costs associated with security incidents stemming from silent transport downgrades or SSRF abuse of diagnostics
Compliance: Avoid processing or persisting sensitive VC/VP data through downgraded, weaker-assurance transports without user awareness
Functional: Accurately report per-transport health (up/down/unknown) instead of assuming advertised transports are functional; Provide a read-only connection self-test that infers CORS refusal without relying on unreliable browser error messages
Operational: Ensure diagnostics do not degrade wallet responsiveness or unnecessarily wake the offscreen document; Keep transport-health reporting isolated from page-facing APIs to avoid RP enumeration of user infrastructure
Business Impact Analysis (3)
BIA-1: Inbound Approval/Consent Request Delivery (Critical)
The wallet's inbox session (mediator-based) must remain reachable so relying parties' approval/consent requests can be delivered to the user in real time.
MTD: 00 days 04:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:15 hours
- Stakeholders: Extension Maintainers / Mediator Operators / Relying Parties / Wallet Users
- Dependencies: DID Resolution Service / Mediator Authentication Endpoint / Mediator WebSocket Service / Offscreen Document Runtime
- Disruptions: Mediator CORS misconfiguration blocking the auth handshake / Mediator outage or WebSocket refusal / Malicious or compromised offscreen dependency tampering with session state
- Impacts: Users miss time-sensitive approval requests, potentially causing failed transactions or unauthorized actions elsewhere / Loss of user trust in wallet reliability / Support burden increase from undiagnosable connectivity complaints
BIA-2: Accurate Transport Status Reporting (High)
The wallet must accurately reflect which transport is actually carrying traffic rather than which transports the DID document merely advertises.
MTD: 01 days 00:00 hours | RTO: 00 days 08:00 hours | RPO: 00 days 01:00 hours
- Stakeholders: Extension Maintainers / Wallet Users
- Dependencies: buildVtaSession Session Builder / TransportHealth State Store / Popup UI (app-shell.tsx)
- Disruptions: Silent transport downgrade to REST without adequate warning / Race condition corrupting health state during concurrent session builds
- Impacts: Users unknowingly transact over a lower-assurance transport / Erosion of trust in the wallet's status indicators
BIA-3: Connection Self-Test Diagnostics (Medium)
Users and mediator operators must be able to run a read-only self-test to identify and remediate CORS/connectivity misconfigurations.
MTD: 03 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: 01 days 00:00 hours
- Stakeholders: Mediator Operators / Wallet Users
- Dependencies: Offscreen Document / DID Resolver / checkCorsReachable Probe Logic
- Disruptions: Abuse of diagnostics as an SSRF/DoS primitive against arbitrary resolved endpoints / Malformed vtaDid causing resolver exceptions
- Impacts: Extension origin used to probe third-party/internal infrastructure without consent / Wasted mediator resources from repeated probing
Technical Scope
Roles (2): RO-1 Wallet User · RO-2 Mediator Operator
Actors (3): AC-1 Wallet User · AC-2 Background Service Worker · AC-3 Offscreen Document Runtime
Entry Points (4): EP-001 RUNTIME_RUN_DIAGNOSTICS Listener · EP-002 RUNTIME_TRANSPORT_HEALTH Listener · EP-005 Mediator Auth Challenge Endpoint · EP-006 CORS Reachability Probe
Threat Actors (3): TA-1 Malicious Browser Extension / Compromised Page Script · TA-2 Malicious or Coerced Mediator Operator · TA-3 Supply Chain Attacker
Infrastructure (2): IF-1 Browser Extension Runtime · IF-2 Externally Operated Mediator Infrastructure
Trust Boundaries (3): TB-1 Extension Internal Message Bus · TB-2 Extension-to-Mediator Network Boundary · TB-3 Extension-to-RP-Page Boundary
External Entities (2): EE-1 Mediator Operator Infrastructure · EE-2 Relying Party Web Page
System Components (5): COMP-001 Background Service Worker · COMP-002 Offscreen Document · COMP-004 Popup UI (AppShell / DiagnosticsPanel) · COMP-006 Mediator Service (External) · COMP-007 Diagnosed Endpoint (authEndpoint/mediator host)
Resources And Assets (3): RA-1 TransportHealth State (byVta map) · RA-2 DiagnosticsReport · RA-3 VC/VP Exchange Payloads
Technologies And Dependencies (2): TD-1 Chrome Extension Manifest V3 Offscreen API · TD-2 React
Use Cases (2)
- Viewing Transport and Inbox Health: A wallet user opens the popup, and AppShell queries the last-observed TransportHealth and inbox session state via useTransportHealth to display which transport is actually carrying traffic and whether
- Running the Connection Self-Test: A wallet user, troubleshooting connectivity, clicks 'Run checks' in DiagnosticsPanel, which starts the offscreen document if needed and performs read-only CORS/reachability/auth-handshake checks again
📋 Risk Registry (5)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Unauthenticated diagnostics message surface can be abused as an SSRF/DoS primitive against mediator and internal infrastructure. | High | Medium | Immediate | Medium |
| RISK-002 | Silent transport downgrade to REST can expose VC/VP exchange to a lower-assurance channel without adequate user warning. | High | Medium | Short-Term | High |
| RISK-003 | Cross-context trust between background and offscreen document lacks integrity verification, enabling spoofed health/status data if either context is compromised. | Medium | Medium | Medium-Term | Medium |
| RISK-004 | Concurrency and stale-state issues in transport health recording can mislead users about current connection status. | Medium | Low | Medium-Term | Low |
| RISK-005 | Diagnostics report and clipboard export can leak infrastructure topology beyond the intended mediator operator audience. | Low | Low | Long-Term | Low |
⚔️ Attack Scenarios (1)
COMP-001: 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 Browser Extension / Compromised Page Script<br><i>Abuse unauthenticated diagnostics surface</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S1@{ shape: rect, label: "STRIDE-1: Unauthenticated Runtime Message Spoofing<br><i>Medium / Likely</i>" }
S8@{ shape: rect, label: "STRIDE-8: Unvalidated Type Assertions Enable Payload Injection<br><i>Medium / Likely</i>" }
S6@{ shape: rect, label: "STRIDE-6: Missing Rate Limiting Triggers Resource Exhaustion<br><i>Low / Possible</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
C1@{ shape: rect, label: "CAPEC-148: Content Spoofing" }
C2@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
C3@{ shape: rect, label: "CAPEC-125: Flooding" }
end
subgraph SL4["4. Weaknesses"]
direction LR
W1@{ shape: rect, label: "CWE-862: Missing Authorization" }
W2@{ shape: rect, label: "CWE-20: Improper Input Validation" }
W3@{ shape: rect, label: "CWE-770: Missing Resource Limits" }
end
subgraph SL5["5. System Component"]
direction LR
SC1@{ shape: rect, label: "COMP-001: Background Service Worker" }
end
TA1 --> S1
TA1 --> S8
TA1 --> S6
S1 --> C1
S8 --> C2
S6 --> C3
C1 --> W1
C2 --> W2
C3 --> W3
W1 --> SC1
W2 --> SC1
W3 --> SC1
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#FFA500,stroke-width:2px
linkStyle 2 stroke:#00FF00,stroke-width:2px
linkStyle 3 stroke:#FFA500,stroke-width:2px
linkStyle 4 stroke:#FFA500,stroke-width:2px
linkStyle 5 stroke:#00FF00,stroke-width:2px
linkStyle 6 stroke:#FFA500,stroke-width:2px
linkStyle 7 stroke:#FFA500,stroke-width:2px
linkStyle 8 stroke:#00FF00,stroke-width:2px
linkStyle 9 stroke:#FFA500,stroke-width:2px
linkStyle 10 stroke:#FFA500,stroke-width:2px
linkStyle 11 stroke:#00FF00,stroke-width:2px
📊 Risk Summary
Total Threats: 12
By Severity: Low: 4 · High: 2 · Medium: 6
By Category: Spoofing: 2 · Elevation of Privilege: 2 · Tampering: 6 · Information Disclosure: 4 · Denial of Service: 4 · Repudiation: 1
🎯 Attack Surface
Kill Chain 1: An attacker with the ability to send chrome.runtime messages (via a sibling extension or a scripting flaw) targets the unauthenticated RUNTIME_RUN_DIAGNOSTICS and RUNTIME_TRANSPORT_HEALTH listeners in background.ts (STRIDE-1, STRIDE-8), forcing handleRunDiagnostics to wake the offscreen document and probe an attacker-influenced vtaDid's resolved endpoints (STRIDE-2), turning the wallet's origin-bearing fetch into an SSRF/reconnaissance oracle against mediator or internal infrastructure, and, if repeated, into a resource-exhaustion vector (STRIDE-6). Kill Chain 2: Separately, a mediator operator (malicious or compromised) who omits the extension's origin from cors_allow_origin causes authenticateToMediator's cross-origin challenge POST to fail with an indistinguishable browser TypeError (STRIDE-4), silently collapsing both TSP and DIDComm and leaving buildVtaSession to fall through to REST (STRIDE-3) — a downgrade that, absent strong user-facing warnings or transport-independent payload protection, exposes VC/VP exchange to a weaker channel exactly when an attacker most wants the victim's inbox dark, e.g., to suppress a fraud alert during a coordinated social-engineering attempt (aligns with TA-2's motive). Kill Chain 3: If either the background/offscreen trust boundary is undermined by a compromised transitively-imported dependency (STRIDE-11, TA-3) or a race condition corrupts concurrent TransportHealth writes (STRIDE-12), the popup can be made to display a spoofed or stale 'healthy'/'live' status (STRIDE-5, STRIDE-10), compounding Kill Chain 2 by removing the very warning signal the user would otherwise rely on to detect the downgrade or blackout, while STRIDE-9's absent audit logging and STRIDE-7's clipboard exposure erode both post-incident attribution and confidentiality of the diagnostic evidence needed to unwind the chain.
🛡️ Risk Mitigation Strategy
Priority 1 (Immediate): Close the unauthenticated diagnostics message surface first, since it is the lowest-effort, highest-leverage entry point — add sender.id validation to every RUNTIME_* listener in background.ts, enforce runtime schema validation (not just TypeScript assertions) on vtaDid and all inbound message shapes, and block private/internal IP ranges in any diagnostic fetch target to eliminate the SSRF/DoS primitive underlying RISK-001. Priority 2 (Short-Term): Address the transport-downgrade and CORS-blackout chain (RISK-002) by decoupling message-layer confidentiality/integrity from transport choice so a REST fallback does not weaken VC/VP protection, and by escalating the UI signal (beyond a small Pill) whenever the active transport diverges from the DID-advertised set or the inbox transitions to offline — this is the control gap an attacker with mediator-level influence (TA-2) would most directly exploit. Priority 3 (Medium-Term): Harden the background/offscreen trust boundary and health-state integrity (RISK-003, RISK-004) via cross-context payload schema validation, dependency pinning/SBOM for dynamically imported DID/mediator modules, and generation-tagged health writes to eliminate the race condition, ensuring that even if a lower layer is compromised or stalls, the UI cannot be made to lie about connectivity. Priority 4 (Long-Term): Round out the program with defense-in-depth and forensic controls (RISK-005) — local audit logging of diagnostics invocations for non-repudiation, and a pre-copy disclosure warning on the DiagnosticsPanel clipboard export — so that residual, lower-severity gaps do not silently accumulate into the next incident's blind spot.
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 1 | 2 |
Confirmed (1)
- 🟡 Missing sender validation on RUNTIME_RUN_DIAGNOSTICS / RUNTIME_TRANSPORT_HEALTH chrome.runtime message listeners (triaged INFORMATIONAL→MEDIUM)
Must-Review-By-Human (2)
- 🔵 Unsafe Formatstring (3 occurrences)
- ⚪ Unvalidated DID-resolved endpoint used as SSRF-style fetch target in diagnostics self-test (offscreen.ts / transport-diagnosis.ts) — SUSPECTED, source not fully provided
The incident this came from
A user reported two warnings and a CORS error in
chrome://extensions:They had correctly allowlisted the extension on their VTA — which is why REST still worked. The refusal was from the mediator, a different service they did not operate. Diagnosing it required leaving the wallet entirely and running
curlagainst someone else's server, andcurlis exactly the tool that cannot reproduce it: a terminal sends noOriginheader, so the endpoint answers perfectly and the operator concludes nothing is wrong.Meanwhile the wallet had silently fallen back to REST, its inbox was dark, and the status line was confidently displaying TSP.
Three linked defects
1. Advertisement is not availability
buildVtaSessionskips a channel whose mediator it cannot reach and falls through to the next — butactiveTransportderived its answer from the stored connection alone, so the UI named transports that had never carried a byte. Thetransports.tsheader already said a status display that disagrees with the router "is worse than none"; it was that.activeTransportnow takes aTransportHealthrecorded bybuildVtaSessionat the two places that actually decide, and skips adowntransport exactly as the session does.Three states, and the third is load-bearing:
updownunknownRestChannelis built from a URL without contacting anything, so construction proves nothingMarking a constructed REST channel
upwould reintroduce the same overconfidence one layer down.unknownis not a failure and never removes REST from selection.With nothing observed yet the answer is unchanged — only the label moves, from "Transport in use" to "Transport expected".
2. A CORS refusal is unreadable, so it is inferred
Chrome hands JS a bare
TypeError: Failed to fetchfor a policy refusal, a dead host and a DNS failure alike. The actual reason goes to the devtools console and nowhere an extension can read — it cannot be recovered from the exception, sotransport-diagnosis.tsinfers it from one bit instead:Errors are discriminated structurally —
TypeError,DOMException.name === "TimeoutError"— never on message text (R3.7). Codes:origin-not-allowed,unreachable,timeout,rejected,unknown.The remediation names the mediator's own config key and deliberately never suggests a host permission (there is a test asserting it doesn't). A grant would exempt the REST handshake but not the WebSocket upgrade, which the mediator checks server-side against the same policy — a half-fix that sends people down the wrong path is worse than no suggestion.
3. The inbox has its own row
"Connected" and "can be reached" are different states, and only the second decides whether an approval request ever arrives (R7.2). A wallet that has fallen back to REST looks entirely healthy on the operating row while nothing can be pushed to it. The state was already tracked in
mediatorState; it just had no UI.The self-test
runDiagnosticswalks the chain and names the broken link, from the wallet's own origin — the only place the answer is true. Read-only throughout: nothing authenticates, mutates or spends a credential, because a diagnostic that changes state is one people are afraid to run.checkCorsReachableuses a plainGETagainst the same endpoint that fails: no custom headers means no preflight, so the browser checksAccess-Control-Allow-Originon the actual response and a refusal surfaces exactly as it does on the real path. Any status is a pass — a405from a POST-only auth route proves the origin was allowed, which is the only question. Swapping it for a health endpoint would test a different policy than the one that breaks.Output is plain text, not Markdown, because it gets pasted into chat clients and issue trackers that each render differently — and it carries the extension origin verbatim, since that is the one string the recipient must copy exactly:
Also
Corrects the claim in
host-permissions.tsthat the mediator is "not subject to CORS". Its WebSocket is not; the auth handshake that must precede it is two ordinaryfetchcalls, and that is what fails. That comment is why the mediator sat outside the one system built for this problem.CLAUDE.mdgains both invariants, with what breaks them.Pre-merge checklist
npm run lintclean (tsc -b, not--noEmit)npm run buildcleannpm test— 646 tests, 0 failures (31 new)dist/background.jssingle bundle, no dynamicimport(), nochrome.cookies, no staticcontent_scripts, nocookiespermissionwithFetchTimeoutat the injection pointPAGE_FACING_RUNTIME_TYPES— which of a user's transports work is wallet diagnostics, not something an RP page should enumerate