fix(inbox): adopt an unattributed relay, and follow the agent when it moves - #149
Conversation
#148 removed the hardcoded demo mediator and backfilled the agent's relay for wallets that had none. It did not fire, and the reason was in the same file: `setSettings` merged the *defaulted* settings and wrote them back. const current = await getSettings(); // mediatorDid: DEFAULT_… await put(SETTINGS_KEY, { ...current, ...patch }); Under the old default that meant any unrelated write — turning on the passkey lock, toggling TSP preference, saving a step-up default — persisted the demo mediator DID into IndexedDB as though it had been chosen. So wallets carry a stored inbox nobody picked, and `inboxToAdopt` declining to move "an inbox that is already set" declined to move exactly the ones the migration existed for. The rule was right; the value alone was never enough to go on. `mediatorDidSource` records who put the relay there. `operator` is a person typing into Setup → Message routing and is never overridden. `agent` is onboarding or the boot backfill, and is left alone too — the inbox is an address others route to, so it must not chase the active VTA. Absent means nobody is on record, which is the truth for every record written before now, and those adopt the agent's relay once. Self-limiting without a version counter: after one boot every record has a source. The cost, stated rather than buried: an operator who hand-set a mediator before provenance existed has it adopted over, once. With nothing deployed, the alternative is wallets stuck on a relay in someone else's deployment, and the person affected is the one who knows how to set it again. `setSettings` now merges onto the STORED record. That write-back is the root cause and it was not specific to the mediator — every derived default became a persisted value that later code could no longer tell apart from a choice. Also: a boot that adopts nothing because no onboarded agent advertises a relay now says so, instead of looking like it did its job. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The inbox is not an address this wallet owns. A v4 holder is a `did:key`, which carries no service endpoint, and the wallet publishes its mediator to nobody — there is no discovery path. So an executor pushing to this wallet can only hand the message to a mediator it already knows, its own, and the wallet hears it only if it is listening there. "Wherever my agent's relay is" is what the inbox means. Which makes the previous commit's rule half right. Pinning an agent-sourced inbox forever is a wallet that goes dark the day its operator redeploys the mediator, with every check still green — the same failure this thread started with, one deployment later. `followAgentInbox` re-resolves the agent's DID document and moves an `agent`-sourced inbox when the advertised relay has changed, then re-opens the sessions on it. An `operator`-sourced inbox never moves; that override is the whole reason provenance exists. On `onStartup` and `onInstalled`, not per worker spin-up: MV3 respawns the worker on almost any event and a DID-document fetch on each would be a lot of network for a value that changes when someone redeploys. The blank-filling backfill still runs every spin-up — it reads the persisted connection and costs nothing. A document that cannot be read is not evidence the relay moved, so a failed resolve keeps what we have and says so. Known limitation, unchanged by this and worth its own issue: one inbox setting against a multi-VTA wallet. Two agents on different mediators cannot both reach it. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
|
Pushed a second commit after a question from review: mediators change over time in the DID document — should the wallet re-resolve on startup? Checked, and the answer is stronger than freshness. A v4 holder is a That means the inbox is not an independent address this wallet owns. It means "wherever my agent's relay is" — and the first commit's rule of pinning an
On A document that cannot be read is not evidence the relay moved, so a failed resolve keeps what we have and says so rather than treating unreadable as absent. Separate limitation this surfaced, not fixed here and probably worth its own issue: one inbox setting against a multi-VTA wallet. Two agents on different mediators cannot both reach it — whichever the inbox names wins and the other agent's pushes never arrive. |
🛡️ AI Agentic Security Code Review3 AI-confirmed issues, 3 findings need a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #149
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 5 · findings: 7
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (3)🟡 inboxToAdopt() silently overwrites legacy unattributed operator-chosen mediator without confirmation
🧠 AI Triage:
Summary: config.ts's inboxToAdopt() function treats any stored mediatorDid lacking the mediatorDidSource provenance tag as unclaimed and unconditionally adopts the agent-advertised value over it. This is an acknowledged one-time migration cost for legacy records, but it also means an attacker who controls the advertised mediator DID can force a takeover of any wallet still in the legacy state, with no operator confirmation and only a console.info log. 📝 Description: A legacy wallet with a deliberately operator-configured mediator (before provenance tracking existed) can have that choice silently discarded and replaced by whatever the active agent currently advertises, without any confirmation dialog — potentially routing DIDComm traffic to an unintended or attacker-controlled relay. 🧪 Proof of Concept: The final fallthrough Vulnerable lines: 168, 178 🔎 Evidence: 💥 Impact: A legacy wallet with a deliberately operator-configured mediator (before provenance tracking existed) can have that choice silently discarded and replaced by whatever the active agent currently advertises, without any confirmation dialog — potentially routing DIDComm traffic to an unintended or attacker-controlled relay. 🧭 Reachability:
⚖️ Triage Factors:
Attack scenario: Legacy wallets with unattributed mediatorDid records are silently migrated to an agent-advertised value on next boot/onboarding, discarding an operator's original deliberate choice without confirmation. 🔧 Remediation:
Change the default behavior for unattributed-but-set records to preserve the existing value rather than silently adopting the advertised one, and surface a UI prompt so the operator makes an informed, auditable decision about migrating. Vulnerable code: Secure code: Additional recommendations:
🟡 Mediator DID and provenance persisted in unencrypted, integrity-unprotected IndexedDB store
🧠 AI Triage:
🔎 Evidence: 🧭 Reachability:
🟡 Race condition (TOCTOU) between reading and writing wallet settings allows lost updates
🧠 AI Triage:
📝 Description: setSettings performs a non-atomic read-modify-write against IndexedDB: it reads the current stored settings, then writes back a merged object. Multiple concurrent callers (e.g. followAgentInbox triggered on startup/update, plus a concurrent user-initiated setSettings from setup-pane.tsx, plus the onboarding flow in offscreen.ts) can interleave their read and write phases. 🌱 Root Cause: No locking, versioning, or atomic compare-and-swap is used around the read-then-write sequence in setSettings/storedSettings. 🔎 Evidence: 🎯 Attack Scenario: An operator submits a manual mediatorDid via setup-pane.tsx at nearly the same time the extension's onStartup/onInstalled handlers invoke followAgentInbox(), which also calls setSettings. If the agent-sourced write's read happens before the operator's write commits, and the agent-sourced write commits after, the operator's deliberate 'operator' provenance pin can be silently overwritten/lost, subverting the never-override guarantee documented in inboxToAdopt/followAgentInbox.
|
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | packages/extension/src/background.ts:1425 |
| Finding ID | github_pr-9035463a5ea7 |
| CWE | CWE-345, CWE-346, CWE-494 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
| MITRE ATT&CK | T1557 - Adversary-in-the-Middle, T1584 - Compromise Infrastructure |
| CAPEC | CAPEC-151, CAPEC-668 |
| DREAD | 5.6 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | conceptual |
| Detection Source | skill_scan |
🧠 AI Triage:
- Severity reassessed: HIGH → MEDIUM — CWE-345/807 trust issue with code-evidence-confirmed reachability (background.ts:462-463) but exploitation requires a compound precondition — prior local compromise granting IndexedDB write access — which is not attacker-controlled remotely and has no network exposure. Impact is bounded to badge/UI spoofing as a social-engineering pretext, not direct authorization bypass of the DIDComm approval flow itself. This matches medium calibration: CVSS N/A but effectively CVSS 4-6.9 equivalent impact, no confirmed real-world exploit, specific conditions (local compromise) required for exploitation.
- Composite score: 4.8
- Environment: unknown
Summary: background.ts's followAgentInbox() re-resolves the active VTA's DID document on every browser startup/update and, if the extracted mediatorDid differs from the stored value, silently persists it as the wallet's trusted inbox with no signature or authenticity verification. An attacker who can influence DID document resolution (compromised resolver, DNS hijack, malicious webvh host) can redirect all inbound DIDComm traffic to a mediator they control.
📝 Description:
An attacker who can influence DID document resolution can silently redirect a victim wallet's entire inbound DIDComm channel to an attacker-controlled mediator, enabling interception, tampering, or dropping of credential offers, verifiable presentation requests, and step-up authentication messages intended for that wallet — undermining the wallet's core trust model.
🧪 Proof of Concept:
The function trusts whatever handleRefreshVtaTransports returns as live with no signature, DID-method-specific proof, or pinning check against a previously known-good value before persisting it and rebinding the inbound listener.
async function followAgentInbox(): Promise<void> {
const settings = await getSettings();
if (settings.mediatorDidSource === "operator") return; // pinned, deliberately
const vtaDid = await readActiveVtaDid();
if (!vtaDid) return;
let live: string | undefined;
try {
const resp = await handleRefreshVtaTransports({
type: RUNTIME_REFRESH_VTA_TRANSPORTS,
vtaDid,
});
if (!resp.ok) throw new Error(resp.error);
live = resp.result.mediatorDid;
} catch (e) {
console.warn("[pnm inbound] could not re-resolve the agent's relay:", e);
return;
}
if (!live) { ... return; }
if (live === settings.mediatorDid) return;
await setSettings({ mediatorDid: live, mediatorDidSource: "agent" });
...
await startInboundListener();
}
Vulnerable lines: 1425, 1466
🔎 Evidence: packages/extension/src/background.ts:1425
async function followAgentInbox(): Promise<void> {
const settings = await getSettings();
if (settings.mediatorDidSource === "operator") return;
...
const resp = await handleRefreshVtaTransports({ type: RUNTIME_REFRESH_VTA_TRANSPORTS, vtaDid });
live = resp.result.mediatorDid;
...
await setSettings({ mediatorDid: live, mediatorDidSource: "agent" });
💥 Impact:
An attacker who can influence DID document resolution can silently redirect a victim wallet's entire inbound DIDComm channel to an attacker-controlled mediator, enabling interception, tampering, or dropping of credential offers, verifiable presentation requests, and step-up authentication messages intended for that wallet — undermining the wallet's core trust model.
🧭 Reachability:
- Network exposure: public
- Auth barrier: none
- Attack path: EP-001/EP-002 (chrome.runtime.onStartup/onInstalled) → followAgentInbox() → handleRefreshVtaTransports() → DID resolution → setSettings() write at background.ts:~1461 → startInboundListener()
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | medium |
| Business impact | high |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: An attacker who controls/spoofs the VTA's DID document resolution can cause followAgentInbox() to silently adopt an attacker-controlled mediator on browser startup, hijacking the wallet's inbound DIDComm channel.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Add a cryptographic verification step against the resolved DID document (method-specific: signature chain for webvh, self-certifying key material for did:key derivatives) before trusting any extracted service endpoint. Consider surfacing a non-dismissible UI notification for agent-sourced relay migrations rather than only console logging.
Vulnerable code:
await setSettings({ mediatorDid: live, mediatorDidSource: "agent" });
...
await startInboundListener();
Secure code:
// Verify the DID document's authenticity/integrity per its DID method
// (e.g. verify webvh log signatures, or DID:key self-certification chain)
// BEFORE trusting the extracted service endpoint.
const verified = await verifyDidDocumentIntegrity(vtaDid, resp.result.didDocument);
if (!verified) {
console.error("[pnm inbound] DID document failed integrity verification; refusing to adopt relay");
return;
}
// Optionally require explicit operator confirmation on first migration.
await setSettings({ mediatorDid: live, mediatorDidSource: "agent" });
await startInboundListener();
Additional recommendations:
- Pin DID resolution to TLS-verified/DNSSEC-backed transports.
- Add a persistent, tamper-evident audit log for mediator migrations (see VULN-005).
- Require operator confirmation on first automatic migration per session.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND: config.ts (provided in full) shows inboxToAdopt() and getSettings()/setSettings() but background.ts's followAgentInbox() and handleRefreshVtaTransports() implementations are NOT in the provided source_files — only referenced by line numbers/snippets in the finding evidence. The config.ts docblock corroborates the design: 'Moving it when the agent moves its relay is followAgentInbox's job in background.ts, which re-resolves the agent's DID document rather than guessing from a cach
- 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.
🟡 Internal runtime message handler lacks sender-origin validation and rate limiting (potential DoS / forced re-resolution)
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | packages/extension/src/background.ts:1470 |
| Finding ID | github_pr-354e1dfb938f |
| CWE | CWE-306, CWE-400 |
| OWASP | A01:2021 - Broken Access Control |
| MITRE ATT&CK | T1499 - Endpoint Denial of Service |
| CAPEC | CAPEC-133, CAPEC-125 |
| DREAD | 4.6 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | theoretical |
| Detection Source | skill_scan |
🧠 AI Triage:
- Triaged severity: MEDIUM
- CWE-306/CWE-400 code-level finding with confirmed reachability (scanner: is_reachable=true, attack path EP-001) but no CVSS score, no known exploit in the wild, and explicitly low business impact (local DoS only, no credential/PII exposure). Exploitability is medium (requires chrome.runtime access, not fully unauthenticated remote). This does not meet HIGH criteria (no CVSS≥7 equivalent severity, no exploit maturity beyond conceptual, low business impact) — medium severity is accurate and unchanged.
- Composite score: 4.7
- Environment: unknown
Summary: handleRefreshVtaTransports() is reachable via an internal chrome.runtime message with no confirmed sender validation in the provided code, and no rate limiting. Repeated invocation by any code with runtime-messaging access to the extension could exhaust resources via repeated DID-document fetches, or be chained with VULN-001 to force repeated mediator churn.
📝 Description:
Uncontrolled invocation could cause excessive network calls (DID resolution) leading to service worker resource exhaustion, and — combined with VULN-001 — could be abused to force repeated mediator relay churn, destabilizing the wallet's inbound message delivery.
🧪 Proof of Concept:
No sender validation is visible around this handler in the reduced source, and the recon agent's EP-004 explicitly notes auth_required: false for this entry point.
async function handleRefreshVtaTransports(
req: RuntimeRefreshVtaTransportsRequest,
): Promise<RuntimeRefreshVtaTransportsResponse> {
// body not shown in reduced excerpt — assumed to process req.vtaDid directly
}
Vulnerable lines: 1470, 1472
🔎 Evidence: packages/extension/src/background.ts:1470
async function handleRefreshVtaTransports(
req: RuntimeRefreshVtaTransportsRequest,
): Promise<RuntimeRefreshVtaTransportsResponse> {
💥 Impact:
Uncontrolled invocation could cause excessive network calls (DID resolution) leading to service worker resource exhaustion, and — combined with VULN-001 — could be abused to force repeated mediator relay churn, destabilizing the wallet's inbound message delivery.
🧭 Reachability:
- Network exposure: internal
- Auth barrier: none
- Attack path: chrome.runtime.sendMessage (EP-004) → onMessage listener (not shown in excerpt, assumed to route to handleRefreshVtaTransports) → DID resolution → potential setSettings() write via followAgentInbox()
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | low |
| Business impact | low |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: A sender with runtime-messaging access to the extension can repeatedly invoke handleRefreshVtaTransports with no visible authentication or rate limiting, risking resource exhaustion or forced mediator re-resolution.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Validate that MessageSender.id matches the extension's own ID before processing, and apply a debounce/rate-limit per sender to prevent resource-exhaustion abuse.
Vulnerable code:
async function handleRefreshVtaTransports(
req: RuntimeRefreshVtaTransportsRequest,
): Promise<RuntimeRefreshVtaTransportsResponse> {
Secure code:
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
if (sender.id !== chrome.runtime.id) {
sendResponse({ ok: false, error: "unauthorized sender" });
return;
}
if (isRateLimited(sender)) {
sendResponse({ ok: false, error: "rate limited" });
return;
}
handleRefreshVtaTransports(req).then(sendResponse);
return true;
});
Additional recommendations:
- Audit manifest.json for externally_connectable and host_permissions scope to confirm actual exposure.
- Add structured logging of message sender identity for anomaly detection.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND: Only the function signature snippet is provided: 'async function handleRefreshVtaTransports(req: RuntimeRefreshVtaTransportsRequest): Promise {' — the body, and any chrome.runtime.onMessage listener wiring with sender validation, are not in source_files. EVIDENCE NOT FOUND: No onMessage listener code, no sender.id checks, no rate-limiting code was found in any provided file. background.ts's full content was not supplied. CHANGED VS PRE-EXISTIN
- 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.
🔵 Unsafe Formatstring (3 occurrences)
| Field | Detail |
|---|---|
| Severity | LOW |
| Location | packages/extension/src/offscreen.ts:635 |
| Finding ID | github_pr-8e5283452ed4 |
| 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:635, offscreen.ts:1258, offscreen.ts:1267
📝 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 evidence code_snippet field is empty (''), and offscreen.ts's actual content at line 635 was not included in source_files at all — only unrelated offscreen.ts references appear in comments within webauthn-prf-unlock.ts. EVIDENCE NOT FOUND: No console.log/util.format call, no string concatenation pattern, and no surrounding function context could be located anywhere in the provided source files for offscreen.ts. CHANGED VS PRE-EXISTING: Cannot determine whether offscreen.ts li
- 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 #149
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | fix/inbox-adopt-unattributed → 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
This PR introduces a provenance model ('agent' vs 'operator') for the wallet's DIDComm inbox mediator DID, fixes a real defect where setSettings() merged the defaulted settings view and silently persisted a hardcoded demo mediator as if chosen, and adds followAgentInbox() to re-resolve and follow the agent's advertised relay on browser startup/update so agent-sourced (non-pinned) wallets do not go dark when their agent redeploys a mediator.
Diff: +194 / -27 lines
Types: security, bugfix, feature, test
🧩 Affected Components
| Component | Impact | Change | What Changed |
|---|---|---|---|
| Wallet Settings / Config Store (mediatorDid trust model) | critical | modified | Introduced provenance-tagged trust decisions for the inbox mediator DID and fixed a read-modify-write defect that silently persisted default |
| Background Service Worker (automatic relay-following) | critical | new | New followAgentInbox() function runs automatically on browser startup and extension install/update, re-resolving the active VTA's DID docume |
| Offscreen Onboarding Flow | medium | modified | doOnboardConnect now tags automatically-adopted mediator DIDs with 'agent' provenance during onboarding. |
| Setup Pane UI (operator pin) | medium | modified | Manual inbox save now stamps mediatorDidSource: 'operator', making the value permanently immune to automated override. |
📁 File Classifications
packages/extension/src/config.ts
- Type: security
packages/extension/src/background.ts
- Type: security
packages/extension/src/offscreen.ts
- Type: security
packages/extension/src/setup-pane.tsx
- Type: security
packages/extension/tests/wallet-inbox.test.mts
- Type: test
🛡️ STRIDE Threat Model
Identified Threats (10)
🟠 STRIDE-1: DID Document Spoofing via followAgentInbox Re-resolution
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Information Disclosure |
| Severity | High |
| Likelihood | Likely |
| CVSS | 7.7 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | High |
| CWE | CWE-345,CWE-346 |
| CAPEC | CAPEC-151,CAPEC-668 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: followAgentInbox() in background.ts allows mediator DID hijacking via a spoofed or compromised DID document due to lack of authenticity verification on the resolved DID document response, resulting in silent redirection of the wallet's inbound DIDComm channel to an attacker-controlled mediator.
Evidence: packages/extension/src/background.ts:~1405-1470
async function followAgentInbox(): Promise<void> {
const settings = await getSettings();
if (settings.mediatorDidSource === "operator") return;
const vtaDid = await readActiveVtaDid();
...
await setSettings({ mediatorDid: live, mediatorDidSource: "agent" });
await startInboundListener();
Attack Scenario:
- Attacker compromises or spoofs resolution of the active VTA's DID document (e.g., via DNS hijack, malicious webvh host, or compromised did:web resolver) referenced by
readActiveVtaDid(). - On
chrome.runtime.onStartuporonInstalled,followAgentInbox()in background.ts is invoked automatically without user interaction. followAgentInbox()callshandleRefreshVtaTransports({ type: RUNTIME_REFRESH_VTA_TRANSPORTS, vtaDid }), which resolves the DID document and extractsresp.result.mediatorDid.- Because
settings.mediatorDidSource !== 'operator'(default isagentor unset), the checkif (settings.mediatorDidSource === 'operator') return;does not block the update. - The attacker-controlled
livemediator DID differs fromsettings.mediatorDid, soawait setSettings({ mediatorDid: live, mediatorDidSource: 'agent' })persists the attacker's mediator as trusted. startInboundListener()is invoked, reopening the DIDComm inbound channel against the attacker's mediator.- All future RP/executor pushes intended for this wallet route through the attacker-controlled mediator, enabling interception, replay, or message tampering of DIDComm envelopes.
🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-002, EP-004
- Data Flows: DID document resolution -> mediatorDid extraction -> IndexedDB settings write -> inbound listener rebind
Preconditions: Attacker can influence or spoof DID document resolution for the currently active VTA (e.g., compromise of DID method resolver, webvh host, or MITM on resolution transport)., Wallet's mediatorDidSource is not operator (i.e., default or agent-adopted state).
Existing Controls: Operator-pinned inbox (mediatorDidSource === 'operator') is never overridden by this function. • Re-resolution occurs only on startup/install, not on every worker spin-up, limiting exposure window.
Recommended Mitigations: Verify DID document authenticity via DID method-specific cryptographic proofs (e.g., signed webvh log, DID:key self-certification) before trusting mediatorDid. • Require explicit user confirmation before silently migrating an agent-sourced inbox to a new value, at least on first migration per session. • Log and alert (not just console.info) on mediator DID changes so operators can detect unexpected relay migrations. • Pin DID resolution to a known-good transport (TLS-verified, DNSSEC, or signed VDR) to reduce spoofing surface.
🟡 STRIDE-2: One-Time Silent Migration Overwrite of Operator-Set Inbox in inboxToAdopt
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-345,CWE-1188 |
| CAPEC | CAPEC-176 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: inboxToAdopt() in config.ts allows a one-time silent overwrite of a legacy operator-configured mediatorDid due to indistinguishable provenance on records written before the mediatorDidSource field existed, resulting in unauthorized redirection of the wallet's inbox to the agent's advertised relay without operator consent.
Evidence: packages/extension/src/config.ts:~150-172
export function inboxToAdopt(
current: { did?: string | undefined; source?: InboxSource | undefined },
advertised: string | undefined,
): string | undefined {
if (!advertised) return undefined;
if (current.source === "operator") return undefined;
if (current.did && current.source === "agen
Attack Scenario:
- A legacy wallet record exists in IndexedDB with
mediatorDidset (from beforemediatorDidSourceexisted) but nomediatorDidSourcefield, representing a deliberate but unattributed operator choice. - Attacker (or a malicious/compromised agent) advertises a different mediator DID via the VTA's DID document.
- On next boot,
startInboundListener()ordoOnboardConnect()callsinboxToAdopt({ did: settings.mediatorDid, source: settings.mediatorDidSource }, advertised). - Since
current.sourceisundefined(not'operator'and not('did' && 'agent')), the function falls through toreturn advertised;, silently adopting the attacker/agent's mediator. setSettings({ mediatorDid: adopt, mediatorDidSource: 'agent' })persists the change, andconsole.infois the only signal — easily missed by an operator.- The wallet's inbox is now the attacker/agent's relay, and the operator's original deliberate choice is silently lost with no rollback path.
- This applies specifically and only once per legacy record, but attacker with agent/DID-document control can force this exact one-time migration to occur on a targeted wallet by ensuring it's still in the legacy no-provenance state when compromise occurs.
🔎 Threat Clue: Derived from COMP-002 via EP-007, EP-001, EP-005
- Data Flows: Legacy IndexedDB settings record -> inboxToAdopt evaluation -> settings overwrite
Preconditions: Target wallet's settings record predates the mediatorDidSource field (or was otherwise created without provenance)., Attacker controls or can influence the DID document of the currently active VTA/agent.
Existing Controls: The overwrite occurs at most once per legacy record (subsequent writes stamp mediatorDidSource: 'agent', which then triggers the agent-sourced retention path). • Console logging (console.info) records the event, though not surfaced to the UI.
Recommended Mitigations: Require explicit operator re-confirmation (UI prompt) before migrating any legacy unattributed mediatorDid, rather than silent auto-adoption. • Emit a persistent, user-visible notification (not just console.info) when a stored mediator value is overwritten during migration. • Provide a rollback/audit log entry capturing the pre-migration value for operator review.
🟡 STRIDE-3: Unauthenticated Runtime Message Triggering handleRefreshVtaTransports
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.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 | Medium |
| CWE | CWE-306,CWE-400 |
| CAPEC | CAPEC-133,CAPEC-125 |
| OWASP | A01:2021 - Broken Access Control |
Description: RUNTIME_REFRESH_VTA_TRANSPORTS message handler in background.ts allows unauthenticated triggering of DID re-resolution and inbox mediator mutation due to missing sender-origin validation on the chrome.runtime message listener, resulting in denial-of-service or forced mediator churn by any extension component or malicious content script.
Evidence: packages/extension/src/background.ts:~1470-1480
async function handleRefreshVtaTransports(
req: RuntimeRefreshVtaTransportsRequest,
): Promise<RuntimeRefreshVtaTransportsResponse> {
Attack Scenario:
- A malicious or compromised extension page/content script (if the extension exposes
externally_connectableor a vulnerable content script bridge) sends aRUNTIME_REFRESH_VTA_TRANSPORTSmessage. handleRefreshVtaTransports(req)is invoked without validatingchrome.runtime.MessageSenderorigin/id, per EP-004 in recon (auth_required: false).- The handler resolves the VTA DID document and may call
setSettings({ mediatorDid: live, mediatorDidSource: 'agent' })if the result differs from the current value, indirectly triggeringfollowAgentInbox's write path. - Repeated invocation from an attacker-controlled sender floods DID resolution network calls, creating resource exhaustion (DoS) or repeatedly flips the inbox mediator if an attacker can also influence resolution results (chaining with STRIDE-1).
- Because no rate-limiting or sender validation exists, this can be triggered at will by any code able to post a runtime message to the background service worker.
🔎 Threat Clue: Derived from COMP-001 via EP-004
- Data Flows: chrome.runtime message -> handleRefreshVtaTransports -> DID resolution -> settings write
Preconditions: Attacker has code execution in a context able to send chrome.runtime.sendMessage to this extension's background worker (e.g., malicious content script if the extension's manifest permits broad host access, or a compromised sibling extension component).
Existing Controls: Message type namespacing (RUNTIME_REFRESH_VTA_TRANSPORTS) constrains the handler surface to a specific action. • No evidence of externally_connectable in provided files (undetermined from reduced source).
Recommended Mitigations: Validate chrome.runtime.MessageSender.id matches the extension's own ID for all internal runtime messages. • Apply rate-limiting/debouncing to handleRefreshVtaTransports to prevent resource exhaustion via repeated invocation. • Restrict message handler exposure to only trusted internal callers (e.g., via a nonce or session token passed from setup-pane.tsx).
🟡 STRIDE-4: Unencrypted IndexedDB Persistence of Mediator DID and Provenance
| Field | Detail |
|---|---|
| Category | Tampering, Information Disclosure |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 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 | Medium |
| CWE | CWE-311,CWE-732 |
| CAPEC | CAPEC-150,CAPEC-37 |
| OWASP | A02:2021 - Cryptographic Failures |
Description: IndexedDBKVStore in config.ts allows local disclosure and tampering of the wallet's mediatorDid and mediatorDidSource due to storage without encryption at rest, resulting in exposure of routing metadata and trust-provenance state to any process with local browser profile access (malware, other extensions with storage access, or physical access).
Evidence: packages/extension/src/config.ts:~145-230
const SETTINGS_KEY = "pnm/settings/v1";
async function storedSettings(): Promise<Partial<WalletSettings>> {
return (await new IndexedDBKVStore().get<Partial<WalletSettings>>(SETTINGS_KEY)) ?? {};
}
Attack Scenario:
- Attacker gains local access to the browser profile (e.g., malware, another malicious extension with
storagepermission overlap, or physical device access). - Attacker reads the IndexedDB object store keyed
pnm/settings/v1directly from the browser's profile directory or via DevTools/extension inspection. mediatorDidandmediatorDidSourceare read in plaintext, revealing the wallet's DIDComm routing configuration and whether it was operator-pinned or agent-adopted.- Attacker with write access to the same IndexedDB store (e.g., via a co-installed malicious extension with storage permission collision, or a compromised offscreen/background context) directly writes a forged
{ mediatorDid: attackerRelay, mediatorDidSource: 'operator' }record. - Because
mediatorDidSource: 'operator'is now stamped,inboxToAdoptandfollowAgentInboxboth treat this as permanently pinned and will never auto-correct it, even when the legitimate agent later advertises the correct relay. - The wallet's inbox is permanently hijacked to the attacker's mediator, disguised as a deliberate operator choice, requiring manual operator intervention to detect and fix.
🔎 Threat Clue: Derived from COMP-002 via EP-007
- Data Flows: IndexedDBKVStore.put/get on key 'pnm/settings/v1'
Preconditions: Attacker has local code execution or storage access within the same browser profile/origin partition as the extension., No integrity protection (e.g., signature, HMAC) exists on the stored settings blob.
Existing Controls: encryptHolderSecret exists as a distinct setting for holder secrets, implying awareness of sensitive-data handling, though not applied to mediatorDid/mediatorDidSource. • Chrome extension storage partitioning limits access to same-extension contexts under normal browser security model.
Recommended Mitigations: Sign or HMAC-protect the stored settings blob so writes from outside the extension's own write path are detectable. • Consider encrypting the settings record at rest, or at minimum the provenance field, to prevent silent tampering. • Add integrity verification (checksum comparison) on read in getSettings()/storedSettings() to detect out-of-band tampering.
🔵 STRIDE-5: Insufficient Logging of Mediator Migration Events for Repudiation Defense
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Low |
| Likelihood | Likely |
| CVSS | 3.1 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,CWE-223 |
| CAPEC | CAPEC-268 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: Mediator migration events in background.ts and offscreen.ts allow repudiation of unauthorized inbox changes due to reliance solely on ephemeral console.info/console.warn logging without persistent, tamper-evident audit trail, resulting in inability to forensically prove when or why a wallet's inbox mediator changed.
Evidence: packages/extension/src/background.ts:~1445-1465
await setSettings({ mediatorDid: live, mediatorDidSource: "agent" });
console.info(
"[pnm inbound] the agent moved its relay:",
settings.mediatorDid ?? "(none)",
"→",
live,
);
Attack Scenario:
- An inbox migration occurs (via
followAgentInbox,startInboundListenerbackfill, ordoOnboardConnect) that redirects the wallet's DIDComm inbox to a new mediator. - The only record of this event is
console.info/console.warnoutput to the browser's DevTools console, which is not persisted, not exported, and cleared on service worker restart (MV3 workers respawn frequently). - If the migration was malicious (chained with STRIDE-1 or STRIDE-2) or simply operator-confusing, there is no durable evidence trail an operator or incident responder can review after the fact.
- An attacker who triggered an unauthorized migration can deny responsibility, and the operator cannot distinguish a legitimate agent-driven relay move from an attacker-induced one without console access captured in real time.
- Post-incident forensics is effectively impossible once the service worker has respawned, as MV3 workers do not persist console history.
🔎 Threat Clue: Derived from COMP-001, COMP-003 via EP-001, EP-002, EP-005
- Data Flows: Mediator migration decision -> console.info/console.warn (ephemeral)
Preconditions: A mediator migration event occurs (benign or malicious)., No external logging/telemetry pipeline captures these console messages.
Existing Controls: Console logging exists at all migration decision points (informative, but non-durable). • Distinct log messages differentiate adoption, following, and unattributed-warning cases.
Recommended Mitigations: Persist mediator migration events (old value, new value, source, timestamp) to a durable, append-only audit log in IndexedDB or exported telemetry. • Surface migration events to the Setup UI so operators can review a history of inbox changes. • Include a cryptographic hash chain or monotonic counter on audit entries to detect log tampering/deletion.
🟡 STRIDE-6: Race Condition (TOCTOU) Between getSettings Read and setSettings Write in Inbox Migration
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-362,CWE-367 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: Concurrent invocation of startInboundListener, followAgentInbox, and doOnboardConnect allows a TOCTOU race condition due to non-atomic read-modify-write on the IndexedDB-backed settings store, resulting in lost updates or inconsistent mediatorDid/mediatorDidSource pairs.
Evidence: packages/extension/src/config.ts:~224-230
export async function setSettings(patch: Partial<WalletSettings>): Promise<void> {
const stored = await storedSettings();
await new IndexedDBKVStore().put(SETTINGS_KEY, { ...stored, ...patch });
}
Attack Scenario:
- MV3 service worker respawns can trigger
chrome.runtime.onStartupandchrome.permissions.onAdded/onRemovedlisteners in overlapping or rapid succession, each independently callinggetSettings()then latersetSettings(). - Thread A (
followAgentInbox) readssettingswithmediatorDid=X, source=agentat time T0. - Thread B (
doOnboardConnectvia offscreen.ts, e.g., from a concurrent onboarding retry) reads the same stored state at T0+ε and independently computes a differentinboxvalue viainboxToAdopt. - Thread B writes
setSettings({ mediatorDid: Y, mediatorDidSource: 'agent' })at T1. - Thread A, still operating on its stale read, writes
setSettings({ mediatorDid: Z, mediatorDidSource: 'agent' })at T1+ε, silently overwriting Thread B's update sincestoredSettings()/setSettings()perform no optimistic locking or versioning. - The final persisted state reflects whichever write occurred last, non-deterministically, potentially reverting a just-completed legitimate migration or, in adversarial conditions, allowing an attacker to win a race against a legitimate correction and have their forged value persist.
🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002, EP-005
- Data Flows: Concurrent getSettings/setSettings on 'pnm/settings/v1'
Preconditions: Two or more mediator-migration code paths (startInboundListener backfill, followAgentInbox, doOnboardConnect) execute concurrently against the same IndexedDB record., No locking, versioning, or transactional guarantee in IndexedDBKVStore.get/put usage as shown.
Existing Controls: storedSettings() reads the raw stored record rather than the defaulted view, reducing (but not eliminating) one class of the original defect. • Migration paths are individually idempotent given a stable settings snapshot, limiting damage to lost-update rather than corruption.
Recommended Mitigations: Use IndexedDB transactions with read-modify-write atomicity (single transaction spanning get+put) rather than separate get() and put() calls. • Introduce a version/ETag field in WalletSettings and reject writes based on stale reads (optimistic concurrency control). • Serialize mediator-migration logic through a single mutex/queue in the background service worker to prevent concurrent execution of followAgentInbox/startInboundListener/doOnboardConnect.
🔵 STRIDE-7: Missing User Confirmation on Setup Pane Inbox Save Enables UI-Layer Trickery
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 4.5 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-20,CWE-345 |
| CAPEC | CAPEC-98,CAPEC-163 |
| OWASP | A04:2021 - Insecure Design |
Description: The Setup → Message routing save handler in setup-pane.tsx allows an operator to permanently pin an attacker-supplied mediator DID due to lack of input validation, format verification, or confirmation dialog on the free-text inbox field, resulting in a permanently trusted malicious relay that survives all future agent-driven corrections.
Evidence: packages/extension/src/setup-pane.tsx:~207-213
await setSettings({ mediatorDid: inbox.trim(), mediatorDidSource: "operator" });
setSavedInbox(inbox.trim());
setRoutingOpen(false);
Attack Scenario:
- Attacker uses social engineering (fake support instructions, phishing page mimicking VTA onboarding) to convince an operator to paste an attacker-controlled mediator DID string into the Setup pane's inbox field.
- Operator clicks Save;
setSettings({ mediatorDid: inbox.trim(), mediatorDidSource: 'operator' })executes with no format validation (e.g., no DID syntax check, no reachability probe) and no confirmation step. - Because
mediatorDidSourceis now'operator', bothinboxToAdoptandfollowAgentInboxtreat this as permanently pinned —if (settings.mediatorDidSource === 'operator') return;— meaning legitimate future agent relay changes will never auto-correct this. - The wallet's inbound DIDComm channel is now durably routed to the attacker's mediator, requiring the victim to notice and manually revert via the same UI.
- All subsequent RP/executor pushes intended for the victim's wallet are interceptable at the attacker's mediator indefinitely.
🔎 Threat Clue: Derived from COMP-004 via EP-006
- Data Flows: Setup pane form input -> setSettings write ('operator' pinned)
Preconditions: Attacker can socially engineer or trick the operator into entering a malicious DID string., No validation of DID syntax, resolvability, or reachability occurs before persisting as 'operator' source.
Existing Controls: Change is explicit and requires operator action (setBusy, form submission), providing some friction against fully automated attack. • The field is documented in-code as "the one place a person picks a relay," indicating deliberate design intent for operator control.
Recommended Mitigations: Validate DID syntax and, where feasible, resolvability of the mediator DID before allowing save. • Display a confirmation dialog summarizing the security implication of pinning a custom relay ("this wallet will always route inbound messages through this relay"). • Provide an in-UI way to view and revert to the last agent-advertised mediator for comparison before committing an operator override.
🔵 STRIDE-8: Denial of Service via Repeated followAgentInbox DID Resolution Failures
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.8 CVSS:4.0/AV:N/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-400,CWE-405 |
| CAPEC | CAPEC-125,CAPEC-227 |
| OWASP | A05:2021 - Security Misconfiguration |
Description: followAgentInbox() in background.ts allows availability degradation of the inbound DIDComm listener due to unhandled repeated resolution failures on every browser startup/update without backoff, resulting in wasted network calls and delayed listener readiness for legitimate inbound messages.
Evidence: packages/extension/src/background.ts:~1425-1435
try {
const resp = await handleRefreshVtaTransports({ type: RUNTIME_REFRESH_VTA_TRANSPORTS, vtaDid });
if (!resp.ok) throw new Error(resp.error);
live = resp.result.mediatorDid;
} catch (e) {
console.warn("[pnm inbound] could not re-resolve the agent's relay:", e);
return;
}
Attack Scenario:
- Attacker controls or disrupts the network path/host serving the active VTA's DID document (e.g., DNS blackhole, TLS termination failure, rate-limiting the resolver endpoint).
- Every
chrome.runtime.onStartupandchrome.runtime.onInstalledevent triggersfollowAgentInbox(), which callshandleRefreshVtaTransports()and thus attempts DID resolution. - The resolution consistently throws, hitting the
catch (e)branch:console.warn("[pnm inbound] could not re-resolve the agent's relay:", e); return;. - No exponential backoff, circuit breaker, or failure-count tracking exists; each browser restart/update repeats the same expensive resolution attempt.
- If resolution is slow (e.g., attacker-induced timeout rather than fast failure), this delays
startInboundListener()'s effective readiness on every boot, degrading availability of inbound message reception during the resolution window. - Combined with MV3 service worker respawn frequency, an attacker who can reliably stall (not just fail) DID resolution can keep the wallet's inbound listener perpetually delayed.
🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-002
- Data Flows: chrome.runtime.onStartup/onInstalled -> followAgentInbox -> handleRefreshVtaTransports -> DID resolution (network)
Preconditions: Attacker can influence network reachability or latency of the DID document resolution endpoint for the active VTA., No timeout/backoff safeguard exists around the resolution call chain.
Existing Controls: Failure path preserves the existing (last-known-good) mediator rather than clearing it, preventing total inbox loss on resolution failure. • Resolution only runs on startup/install, not per worker spin-up, limiting frequency of exposure.
Recommended Mitigations: Add a bounded timeout to the DID resolution call within handleRefreshVtaTransports invoked from followAgentInbox. • Implement exponential backoff / failure counters to avoid repeated expensive resolution attempts on persistent failure. • Decouple startInboundListener() invocation from followAgentInbox() completion so a slow/failed resolution does not block listener readiness.
🔵 STRIDE-9: Type Confusion in Legacy IndexedDB Record Bypassing mediatorDidSource Validation
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 3.5 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-20,CWE-704 |
| CAPEC | CAPEC-136 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: getSettings() in config.ts allows injection of an invalid mediatorDidSource value due to the type guard only accepting exact string matches without rejecting or sanitizing malformed stored records, resulting in ambiguous provenance state if an attacker writes a crafted IndexedDB record with an unexpected mediatorDidSource value.
Evidence: packages/extension/src/config.ts:~195-200
...(s?.mediatorDidSource === "agent" || s?.mediatorDidSource === "operator"
? { mediatorDidSource: s.mediatorDidSource }
: {}),
Attack Scenario:
- Attacker with local write access to the extension's IndexedDB (e.g., via a compromised co-installed extension able to access the same storage partition, or direct DevTools manipulation) writes
{ mediatorDid: 'did:attacker:relay', mediatorDidSource: 'admin' }(an invalid enum value) directly to thepnm/settings/v1key. getSettings()evaluatess?.mediatorDidSource === 'agent' || s?.mediatorDidSource === 'operator', which is false for'admin', so the field is silently dropped from the returned defaulted view (treated as unset).- However,
storedSettings()(used internally bysetSettings) returns the RAW stored record including the invalidmediatorDidSource: 'admin'value, since it does no filtering. - Downstream code paths that call
getSettings()see nomediatorDidSource, triggering the 'unattributed, adopt' logic ininboxToAdopt/followAgentInbox— butmediatorDiditself (did:attacker:relay) IS preserved bygetSettings()'s spread since it only checks truthiness, not source validity. - Depending on call order, this could either cause the malicious relay to be silently adopted-over (if a legitimate agent relay is available) or persist as the active
mediatorDidwith no clear source designation, creating ambiguous trust state that operators cannot audit via the UI (which likely displaysmediatorDidSourcefor clarity).
🔎 Threat Clue: Derived from COMP-002 via EP-007
- Data Flows: Raw IndexedDB record -> storedSettings (unfiltered) vs getSettings (filtered) -> inboxToAdopt
Preconditions: Attacker has direct write access to the extension's IndexedDB storage outside the normal setSettings() API path., The crafted record uses a mediatorDidSource value outside the InboxSource union type.
Existing Controls: getSettings() defensively validates mediatorDidSource against the exact allowed literal values before including it in the defaulted view. • TypeScript's type system prevents this at the application's own call sites (though not against raw storage tampering, which bypasses type checking entirely).
Recommended Mitigations: Add explicit rejection/sanitization of unrecognized mediatorDidSource values in storedSettings() as well, not just getSettings(). • Validate the entire settings object against a schema (e.g., zod/io-ts) on every read from IndexedDB to reject malformed records outright. • Consider treating a malformed/unrecognized mediatorDidSource as equivalent to 'operator' (fail-safe/restrictive) rather than falling through to unattributed-adopt logic.
⚪ STRIDE-10: Silent Failure Mode on Unattributed Inbox with No VTA Advertisement Leaves Wallet Unreachable
| Field | Detail |
|---|---|
| Category | Denial of Service, Information Disclosure |
| Severity | Informational |
| Likelihood | Likely |
| CVSS | 1.8 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 | None |
| CWE | CWE-756,CWE-390 |
| CAPEC | CAPEC-664 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: startInboundListener() in background.ts allows persistent unreachability of the wallet's inbound DIDComm channel due to the warning-only fallback when no mediator can be adopted or attributed, resulting in silent message-delivery failure that presents as a false-positive healthy state to the user.
Evidence: packages/extension/src/background.ts:~500-512
} else if (!settings.mediatorDidSource && vtaDids.length > 0) {
console.warn(
"[pnm inbound] inbox relay is unattributed and no onboarded agent " +
"advertises one to adopt — refresh the agent's transports, or set a " +
"relay under Setup → Message routing.",
);
}
Attack Scenario:
- Wallet boots with
mediatorDidSourceunset and no VTA (vtaDids.length === 0or resolution otherwise fails to produce an advertised mediator). startInboundListener()executes the else-branch:console.warn("[pnm inbound] inbox relay is unattributed and no onboarded agent advertises one to adopt...").- This warning is only visible in the browser DevTools console for the extension's background context, which the average operator never opens.
- The wallet continues to appear operational in the popup/setup UI (assuming no UI-level surfacing of this specific warning), giving a false sense that inbound messages will be received.
- An RP or executor attempts to push a DIDComm message to this wallet's (nonexistent/stale) inbox and the delivery silently fails or is dropped by the mediator infrastructure, with no feedback loop to either party.
- This is a design/availability gap rather than a directly exploitable vulnerability, but an attacker aware of this behavior could specifically target wallets in this state (e.g., freshly reset or migrated wallets) knowing inbound delivery is guaranteed to fail, to mask a simultaneous phishing/social-engineering attempt requesting the victim manually configure a malicious relay 'to fix connectivity.'
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: startInboundListener -> unattributed warning (console only)
Preconditions: Wallet has no attributed mediator and no VTA to adopt from., Operator does not proactively check DevTools console or Setup pane diagnostics.
Existing Controls: runDiagnostics (referenced in offscreen.ts comments) is stated to report this state, providing at least one discoverable surface. • The design explicitly treats unset as "the honest state" rather than fabricating a default, avoiding a worse false-positive of pointing to a wrong relay.
Recommended Mitigations: Surface the unattributed-inbox warning prominently in the Setup pane UI, not only in DevTools console. • Add a periodic self-check/notification (e.g., badge icon change) when the wallet has been in the unattributed state beyond a threshold duration. • Provide one-click remediation (e.g., a 'Fix inbox routing' button) directly from the warning surface.
🍝 PASTA Threat Model
Application Purpose
A Chrome Manifest V3 browser extension implementing a DIDComm-based wallet ('holder') that receives verifiable credential and step-up-authentication messages via a mediator relay advertised by an associated verifiable trust agent (VTA), providing decentralized identity wallet functionality to end users.
Inherent Risks
- The wallet has no independent discovery mechanism (did:key holders lack service endpoints), making inbound message delivery entirely dependent on trusting a third-party-advertised mediator DID.
- MV3 service worker lifecycle (frequent respawn) increases the number of code paths that must independently and consistently derive trust state from persisted settings.
- IndexedDB storage has no built-in confidentiality or integrity guarantees against other local actors sharing the browser profile.
Objectives
Risk: Accept a bounded, one-time, and clearly-scoped risk of automatic migration for legacy records lacking provenance, in exchange for eliminating a broader class of stuck-wallet failures.
Business: Provide a trustworthy decentralized identity wallet experience for end users interacting with relying parties and verifiable trust agents.
Security: Prevent unauthorized redirection of the wallet's inbound message channel to an attacker-controlled relay.; Preserve deliberate operator configuration choices against silent automated override.
Financial: Avoid liability and remediation costs from wallet compromise incidents affecting credential holders.
Compliance: Maintain data integrity and auditability expectations consistent with decentralized identity trust frameworks (e.g., DIDComm, W3C VC) that these wallets participate in.
Functional: Reliably receive DIDComm-pushed messages (credentials, step-up authentication requests) via a correctly-routed inbox mediator.
Operational: Ensure inbox mediator configuration remains consistent and correctly attributed across browser restarts, extension updates, and onboarding flows.
Business Impact Analysis (2)
BIA-1: Inbound Credential and Step-Up Message Delivery (High)
The end-to-end process by which relying parties and executors push DIDComm-encoded verifiable credentials or step-up authentication requests to a user's wallet via the mediator relay recorded in wallet settings.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Extension Developers / Mediator Relay Operators / Relying Parties (RPs) / Verifiable Trust Agent (VTA) Operators / Wallet Operators (End Users)
- Dependencies: Chrome Extension Runtime (MV3) / DID Document Resolution Infrastructure / DIDComm Mediator Relay Service / IndexedDBKVStore (@openvtc/pnm-core)
- Disruptions: Attacker-controlled or compromised DID document resolution redirecting the mediator / Legitimate mediator migration racing with a concurrent onboarding write / Mediator relay outage or unreachability / Silent unattributed-inbox state leaving the wallet permanently unreachable
- Impacts: Credential delivery failure requiring manual operator remediation / Interception or tampering of DIDComm-pushed step-up authentication requests, enabling account takeover of downstream RP sessions / Loss of user trust in the wallet's reliability / Potential regulatory scrutiny if step-up authentication bypass leads to unauthorized transaction approval
BIA-2: Operator-Initiated Inbox Mediator Configuration (Medium)
The process by which a wallet operator deliberately sets or overrides the inbox mediator DID via the Setup pane's Message routing section, expected to persist as the authoritative, non-overridden configuration.
MTD: 07 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Setup Pane UI Maintainers / Wallet Operators (End Users)
- Dependencies: IndexedDBKVStore (@openvtc/pnm-core) / React Setup Pane Component (setup-pane.tsx)
- Disruptions: Social engineering leading operator to pin a malicious mediator DID / Lack of validation allowing malformed or unreachable DIDs to be saved
- Impacts: Permanent (until manual correction) redirection of inbound messages to an attacker relay / Operator confusion when the pinned value silently diverges from the agent's advertised relay
Technical Scope
Roles (3): RO-1 Wallet Operator · RO-2 Verifiable Trust Agent Operator · RO-3 Extension Background Process
Actors (4): AC-1 Wallet Operator (Human) · AC-2 Background Service Worker · AC-3 Offscreen Onboarding Process · AC-4 Verifiable Trust Agent
Use Cases (3): Automatic Inbox Mediator Adoption on Onboarding · Operator-Initiated Mediator Pinning · Boot-Time Mediator Following
Attack Trees (4): SC-1: Background Service Worker · SC-2: Wallet Settings Store (config.ts) · SC-3: IndexedDB Persistent Store · SC-5: Setup Pane UI
Entry Points (7): EP-001 Browser Startup Event · EP-002 Extension Install/Update Event · EP-003 Permissions Change Event · EP-004 Refresh VTA Transports Runtime Message · EP-005 Onboarding Connect Function · EP-006 Setup Pane Save Inbox Handler · EP-007 Inbox Adoption Decision Function
Risk Registry (7): RISK-001 · RISK-002 · RISK-003 · RISK-004 · RISK-005 · RISK-006 · RISK-007
Threat Actors (3): TA-1 Malicious DID Resolution Infrastructure Operator · TA-2 Local Browser Profile Malware / Co-Installed Malicious Extension · TA-3 Social Engineer
Infrastructure (1): IF-1 Browser Extension Client Runtime
Trust Boundaries (3): TB-1 Browser Extension Runtime Boundary · TB-2 External DID Resolution Network · TB-3 Operator/User Interaction Boundary
External Entities (3): EE-1 Relying Party / Executor · EE-2 Verifiable Trust Agent (VTA) · EE-3 DID Document Host/Resolver
System Components (7): SC-1 Background Service Worker · SC-2 Wallet Settings Store (config.ts) · SC-3 IndexedDB Persistent Store · SC-4 Offscreen Onboarding Document · SC-5 Setup Pane UI · SC-6 VTA DID Document Resolver · SC-7 DIDComm Mediator Relay
Resources And Assets (3): RA-1 Wallet Settings Record (pnm/settings/v1) · RA-2 Active VTA DID Document · RA-3 Inbound DIDComm Message Channel
Technologies And Dependencies (3): TD-1 @openvtc/pnm-core (IndexedDBKVStore) · TD-2 Chrome Extension Manifest V3 APIs · TD-3 React
⚔️ Attack Scenarios (1)
Exploit identified weaknesses
flowchart LR
S0["DID Document Spoofing via followAgentInbox Re-resolution"]
S1["One-Time Silent Migration Overwrite of Operator-Set Inbox in"]
S2["Unauthenticated Runtime Message Triggering handleRefreshVtaT"]
S0 --> S1
S1 --> S2
📊 Risk Summary
Total Threats: 10
By Severity: Low: 4 · High: 1 · Medium: 4 · Informational: 1
By Category: Spoofing: 2 · Tampering: 7 · Information Disclosure: 3 · Elevation of Privilege: 2 · Denial of Service: 3 · Repudiation: 1
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 3 | 3 |
Confirmed (3)
- 🟡 inboxToAdopt() silently overwrites legacy unattributed operator-chosen mediator without confirmation
- 🟡 Mediator DID and provenance persisted in unencrypted, integrity-unprotected IndexedDB store
- 🟡 Race condition (TOCTOU) between reading and writing wallet settings allows lost updates
Must-Review-By-Human (3)
- 🟡 Mediator DID adopted from unauthenticated DID document without integrity verification (relay hijack) (triaged HIGH→MEDIUM)
- 🟡 Internal runtime message handler lacks sender-origin validation and rate limiting (potential DoS / forced re-resolution)
- 🔵 Unsafe Formatstring (3 occurrences)
Follow-up to #148, which did not fire. Reported from a self-test still showing
inbox mediator → https://mediator.vtc.storm.wson a wallet whose agent isdids.eu.openvtc.net, after a clean pull, build and extension restart.Why the migration didn't fire
Not the rule — the data. It was in the same file:
setSettingsmerged the defaulted settings and wrote them back. Under the old hardcoded default, any unrelated write — turning on the passkey lock (encrypt-holder.ts), toggling TSP preference, saving a step-up default — persisted the demo mediator DID into IndexedDB as though a person had chosen it.So affected wallets don't have an unset inbox. They have a stored one, byte-identical to a deliberate choice, and
inboxToAdoptdeclining to move "an inbox that is already set" declined to move precisely the wallets the migration existed for. The rule was right; keying it on the value alone was never enough.The fix
mediatorDidSourcerecords who put the relay there:operatoragentSelf-limiting without a version counter: after one boot every record has a source.
The cost, stated rather than buried: an operator who hand-set a mediator before provenance existed has it adopted over, once. With nothing deployed, the alternative is wallets stuck on a relay in someone else's deployment — and the person affected is exactly the one who knows how to set it again.
Root cause fixed too
setSettingsnow merges onto the stored record via astoredSettings()helper. The write-back was not specific to the mediator: every derived default (encryptHolderSecret: false,preferTsp: true) became a persisted value that later code could no longer tell apart from a choice. That is the shape of the bug, not one instance of it.Also
A boot that adopts nothing because no onboarded agent advertises a relay now warns, instead of looking like it did its job. That is the remaining way this can silently no-op: the backfill reads the mediator off the persisted connection, so a connection recorded without one has nothing to adopt. Refreshing the agent's transports re-resolves the DID document and fills it in.
Verification
Lint, build and 685 tests pass; two new cases pin the two that matter — adopting over an unattributed stored relay, and not re-adopting on the next boot.
Not verified by me: the live path. After this one, the self-test's inbox row should name
mediator.eu.openvtc.net. If it still doesn't, the service-worker console will now say which of the two reasons it is.Pre-merge checklist