Skip to content

feat(transports): report the transport actually in use, and why one is not - #143

Merged
stormer78 merged 1 commit into
mainfrom
feat/transport-diagnostics
Aug 30, 2026
Merged

feat(transports): report the transport actually in use, and why one is not#143
stormer78 merged 1 commit into
mainfrom
feat/transport-diagnostics

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The incident this came from

A user reported two warnings and a CORS error in chrome://extensions:

[pnm tsp] skipping TSP channel: Failed to fetch
[pnm didcomm] skipping DIDComm channel: Failed to fetch
Access to fetch at 'https://mediator.…/mediator/v1/authenticate/challenge'
from origin 'chrome-extension://…' has been blocked by CORS policy

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 curl against someone else's server, and curl is exactly the tool that cannot reproduce it: a terminal sends no Origin header, 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

buildVtaSession skips a channel whose mediator it cannot reach and falls through to the next — but activeTransport derived its answer from the stored connection alone, so the UI named transports that had never carried a byte. The transports.ts header already said a status display that disagrees with the router "is worse than none"; it was that.

activeTransport now takes a TransportHealth recorded by buildVtaSession at the two places that actually decide, and skips a down transport exactly as the session does.

Three states, and the third is load-bearing:

state meaning
up positive evidence — for TSP/DIDComm a completed mediator handshake and an open socket
down skipped, with a diagnosed reason
unknown REST: a RestChannel is built from a URL without contacting anything, so construction proves nothing

Marking a constructed REST channel up would reintroduce the same overconfidence one layer down. unknown is 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 fetch for 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, so transport-diagnosis.ts infers it from one bit instead:

a request that fails at the network layer, against a host that answers an opaque (mode: "no-cors") request a moment later, was refused by browser policy, not by the network.

Errors are discriminated structurallyTypeError, 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

runDiagnostics walks 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.

checkCorsReachable uses a plain GET against the same endpoint that fails: no custom headers means no preflight, so the browser checks Access-Control-Allow-Origin on the actual response and a refusal surfaces exactly as it does on the real path. Any status is a pass — a 405 from 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:

[FAIL] TSP+DIDComm mediator accepts this wallet's origin
       The mediator at https://mediator.example is up and answering, but
       refused this request. …
       code: mediator/origin-not-allowed
       fix: Whoever operates this mediator needs to add the origin to
       `[security] cors_allow_origin` in its `mediator.toml` and restart it. …

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 fetch calls, and that is what fails. That comment is why the mediator sat outside the one system built for this problem.

CLAUDE.md gains both invariants, with what breaks them.

Pre-merge checklist

  • npm run lint clean (tsc -b, not --noEmit)
  • npm run build clean
  • npm test — 646 tests, 0 failures (31 new)
  • MV3 invariants: dist/background.js single bundle, no dynamic import(), no chrome.cookies, no static content_scripts, no cookies permission
  • Module boundaries + entry points unchanged (core untouched)
  • R1.2 — every new fetch bounded via withFetchTimeout at the injection point
  • R3.7 — new codes are stable constants; no matching on message text
  • New runtime message is not in PAGE_FACING_RUNTIME_TYPES — which of a user's transports work is wallet diagnostics, not something an RP page should enumerate

…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>
@stormer78
stormer78 merged commit 334f858 into main Aug 30, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/transport-diagnostics branch August 30, 2026 08:32
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Aug 30, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

1 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

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/transport-diagnosticsmain
Validated 2026-09-05
Scan ID 0098de3a
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 2 · with findings: 1 · files: 16 · findings: 3

Module Files scanned Findings
packages/extension 15 3
(root) 1 0

Executive Summary

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

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


🔒 Security Issues

Confirmed Vulnerabilities (1)

🟡 Missing sender validation on RUNTIME_RUN_DIAGNOSTICS / RUNTIME_TRANSPORT_HEALTH chrome.runtime message listeners

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:1959
Finding ID github_pr-f99fb45b7eac
CWE CWE-346, CWE-862
OWASP A01:2021 - Broken Access Control
MITRE ATT&CK T1055.001 (Process Injection analog — extension IPC abuse), T1071 (App Layer Protocol abuse of internal message channel)
CAPEC CAPEC-148, CAPEC-668
DREAD 5.2
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: INFORMATIONAL → MEDIUM — The scanner code evidence conclusively shows the missing sender.id check (CWE-346/862) and confirms the message-listener code path is reachable and triggers real handlers (handleRunDiagnostics, ensureOffscreenDocument). This is more than purely informational — it's a concrete, fixable authorization gap — but exploitation requires the attacker to already have chrome.runtime messaging access to this extension (not reachable from arbitrary web content under MV3 isolation), and the business impact is explicitly assessed as low (diagnostic activity/resource use only, no credential or key exposure). This combination — real, reachable code flaw but constrained precondition and low impact — supports 'low' rather than remaining purely informational or escalating to medium/high.
  • Composite score: 5
  • Environment: production

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 sender.id or sender.origin is performed before dispatching to handleRunDiagnostics/handleTransportHealth, meaning the dispatch trusts the message.type field alone regardless of who sent it.

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  ...
  if ((message as { type?: string })?.type === RUNTIME_RUN_DIAGNOSTICS) {
    handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest)
      .then(sendResponse)
      .catch((e: unknown) =>
        sendResponse({ ok: false, error: e instanceof Error ? e.message : String(e) }),
      );
    return true; // async sendResponse
  }

  if ((message as { type?: string })?.type === RUNTIME_TRANSPORT_HEALTH) {
    handleTransportHealth()
      .then(sendResponse)
      .catch(...);
    return true;
  }
  ...
});

Vulnerable lines: 1959, 1974

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

if ((message as { type?: string })?.type === RUNTIME_RUN_DIAGNOSTICS) {
    handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest)
      .then(sendResponse)
      .catch(...);
    return true; // async sendResponse
  }

  if ((message as { type?: string })?.type === RUNTIME_TRANSPORT_HEALTH) {
    handleTransportHealth()
      .then(sendResponse)
      ...

💥 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:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001 (chrome.runtime.onMessage RUNTIME_RUN_DIAGNOSTICS) → background.ts message listener (no sender check) → handleRunDiagnostics() → ensureOffscreenDocument() → OFFSCREEN_RUN_DIAGNOSTICS

⚖️ Triage Factors:

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

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:

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

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:

if ((message as { type?: string })?.type === RUNTIME_RUN_DIAGNOSTICS) {
  handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest).then(sendResponse)...
}

Secure code:

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (sender.id !== chrome.runtime.id) {
    // Reject cross-extension / unexpected senders outright.
    return;
  }
  if ((message as { type?: string })?.type === RUNTIME_RUN_DIAGNOSTICS) {
    if (typeof (message as RuntimeRunDiagnosticsRequest).vtaDid !== 'string') {
      sendResponse({ ok: false, error: 'invalid vtaDid' });
      return true;
    }
    handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest)
      .then(sendResponse)
      .catch((e: unknown) => sendResponse({ ok: false, error: e instanceof Error ? e.message : String(e) }));
    return true;
  }
  ...
});

Additional recommendations:

  • Rate-limit RUNTIME_RUN_DIAGNOSTICS invocations per popup session
  • Consider not exposing externally_connectable if not required
  • Log/alert on repeated diagnostics invocations from unexpected contexts

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: background.ts shows 'if ((message as { type?: string })?.type === RUNTIME_RUN_DIAGNOSTICS) { handleRunDiagnostics(message as RuntimeRunDiagnosticsRequest)...}' and similarly for RUNTIME_TRANSPORT_HEALTH — the code snippet shows dispatch purely on message.type with no reference to sender.id, sender.origin, or any allowlist check in the visible handler body. EVIDENCE NOT FOUND: No chrome.runtime.onMessage wrapper code showing a sender validation check (e.g. `sender.id !== chrome.ru
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

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

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

🔵 Unsafe Formatstring (3 occurrences)

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.

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

🛡️ Threat Model & Affect Analysis — PR #143

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/transport-diagnosticsmain
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:

  1. 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.
  2. A malicious or compromised content script / another extension with externally_connectable access sends a message {type: RUNTIME_RUN_DIAGNOSTICS, vtaDid: }.
  3. handleRunDiagnostics(req) is invoked, calling ensureOffscreenDocument() and forwarding OFFSCREEN_RUN_DIAGNOSTICS to the offscreen document with attacker-controlled vtaDid.
  4. The offscreen document resolves the attacker-supplied vtaDid and performs live network probes (checkCorsReachable, authenticateToMediator challenge POST) against endpoints derived from that DID.
  5. 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:

  1. Attacker crafts or registers a malicious did:webvh / did:web document that resolves to attacker-controlled authEndpoint and mediator URLs.
  2. 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.
  3. runDiagnostics (offscreen.ts, referenced in CLAUDE.md) resolves the DID and performs checkCorsReachable GET and authenticateToMediator POST {authEndpoint}/challenge against attacker-controlled infrastructure.
  4. 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.
  5. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

  1. 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.
  2. 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.
  3. Both TSP and DIDComm transports fail because they share this same auth handshake, silently leaving REST as the only carrier.
  4. 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.
  5. 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:

  1. 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.
  2. 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.
  3. The attacker returns a crafted TransportHealthResult marking all transports 'up' and the inbox session 'live' even though the mediator handshake actually failed.
  4. app-shell.tsx renders this as Pill tone='ok' Live, giving the victim false confidence that inbound approval requests can be delivered.
  5. 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:

  1. A malicious page or compromised extension repeatedly sends RUNTIME_RUN_DIAGNOSTICS messages in a tight loop (bounded only by message-passing throughput).
  2. Each call triggers handleRunDiagnostics -> ensureOffscreenDocument() -> OFFSCREEN_RUN_DIAGNOSTICS, which performs checkCorsReachable and authenticateToMediator network calls per run.
  3. 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.
  4. 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.
  5. 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:

  1. User runs the connection self-test and clicks Copy in DiagnosticsPanel (diagnostics-panel.tsx).
  2. navigator.clipboard.writeText(formatReport(report)) places the full DiagnosticsReport — including extensionOrigin and mediator/authEndpoint hostnames plus remediation text — onto the system clipboard.
  3. 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.
  4. 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.
  5. 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:

  1. TypeScript type assertions (message as RuntimeRunDiagnosticsRequest) are compile-time only and provide zero runtime guarantee about the actual shape of message.
  2. 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.
  3. handleRunDiagnostics forwards req.vtaDid directly into the OFFSCREEN_RUN_DIAGNOSTICS message without validation.
  4. 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.
  5. 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:

  1. No logging mechanism is visible in handleRunDiagnostics or handleTransportHealth beyond ephemeral in-memory state and console (implicit).
  2. An attacker abusing RUNTIME_RUN_DIAGNOSTICS for SSRF-style probing (see STRIDE-2) or DoS (STRIDE-6) leaves no durable record within the extension.
  3. 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.
  4. 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:

  1. 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).
  2. 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).
  3. The user, seeing a non-alarming 'Connecting' status rather than a warn-flagged failure, does not investigate further or run the connection self-test.
  4. 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:

  1. background.ts comments confirm offscreen.ts performs DID resolution and mediator session setup via import()/DOM APIs not available in the service worker.
  2. 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.
  3. 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.
  4. 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:

  1. User triggers a manual reconnect (e.g., via Setup pane) while a background-initiated session rebuild for the same vtaDid is already in flight.
  2. Both invocations of buildVtaSession race to write TransportHealth entries keyed by vtaDid in the shared byVta map maintained by the offscreen document.
  3. 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.
  4. 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.
  5. 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
Loading

📊 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 ⚠️ Must-Review-By-Human
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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants