fix(inbox): take the wallet's inbox from its agent, not a hardcoded relay - #148
Conversation
…elay `DEFAULT_WALLET_MEDIATOR_DID` was a demo mediator on a domain no deployment in use here runs, substituted by `getSettings()` whenever `mediatorDid` was unset. The only writer of that setting was the advanced "Change routing" field in Setup — onboarding never wrote it — so every wallet whose operator never opened that field ran its inbox through a third party's host, whichever agent it had onboarded to. Inbound consent requests and approvals are what that relay carries. Setup meanwhile told the operator the inbox was "set up automatically from your agent. One relay carries messages in both directions". Nothing did it. A claim not backed by evidence (guide §0), covering for the routing it misdescribed. Onboarding now adopts the agent's advertised DIDComm mediator, and the constant is gone rather than repointed. `inboxToAdopt` holds the rule in one place: adopt when the wallet has none, decline when it already has one — an operator running two relays meant it, and the inbox is an address other parties already route to, so a second onboarding silently moving it would strand everyone who knows this wallet. Unset is now a real state and is reported as one. `getWalletMediatorDid` returns `string | undefined`; the paths that must open a session throw `NoInboxMediatorError` (code `wallet/inbox-not-configured`, matched structurally per R3.7); `reconcileInbound` returns with one clear line rather than feeding every VTA into a backoff loop that cannot fix a setting; and the self-test fails with "nothing can reach this wallet" instead of proving a stranger's relay healthy and calling it a pass. Wallets onboarded before this have the agent's mediator on their persisted `Connection` but no setting. Re-onboarding to acquire one mints a fresh holder DID and invalidates every RP ACL, which is not a fair ask, so `startInboundListener` backfills from the connection on every boot — the same rule, at the one place that always runs. This mirrors `tspMediatorDid`, backfilled onto existing connections for the same reason. Also fixed while here, all in the same blast radius: - The inbox memo in `offscreen.ts` was justified as keeping the "is this our inbox?" check synchronous inside `onClose`, but that check is awaited into `isInbox` before the closure is built. It bought nothing and went stale whenever settings were written from the options page, a different context. Removed; IndexedDB is shared. - Changing the inbox by hand now re-opens the inbound sessions (`RUNTIME_RESTART_INBOX`). The reconcile otherwise runs only on boot and on a connection change, so Setup reported "Saved." over a wallet still listening on the old relay. - Setup names the relay it claims to have configured, so the sentence is checkable where it is made. - The re-mint warning guarding that field described v3, where the holder was a `did:peer:2` with the mediator encoded inline. A v4 holder is a VTA-minted `did:key` carrying no mediator and `saveInbox` mints nothing, so the scariest warning in Setup guarded a consequence that no longer happens — while deterring people from fixing the routing it was describing. It now states what actually changes. `tests/wallet-inbox.test.mts` pins the adopt rule, the backfill parser, and — verified to fail on a reintroduction — that no DID literal with a real identifier body ships in `src`. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… moves (#149) * fix(inbox): adopt over a relay nobody is on record choosing #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> * fix(inbox): follow the agent when it moves its relay 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> --------- Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The invariant behind #148–#150, written down where the next person to touch `reconcileInbound` will find it. Without it a map where a string would do reads as over-built, and collapsing it back is a one-line change whose only symptom is one agent's consent requests quietly never arriving. The load-bearing fact is not obvious from any single file: a v4 holder is a `did:key` with no service endpoint and the wallet publishes its relay to nobody, so there is no discovery path — an executor pushes through the relay IT knows, and the wallet hears it only if it is listening there. Everything else (the pair keying, the provenance field, the two orderings) follows from that and looks arbitrary without it. Also corrects the intro: the wallet runs one inbound session per onboarded agent, not "the" session. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review2 AI-confirmed issues, 1 finding needs a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #148
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 9 · findings: 3
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (2)🟡 Inbox mediator auto-adopted from unauthenticated agent-advertised data without integrity/authorization check
🧠 AI Triage:
📝 Description: The background service worker automatically writes the agent-advertised mediator DID into the wallet's persistent inbox setting on every boot, with no confirmation step or authorization check beyond 'is the setting currently unset'. 🌱 Root Cause: inboxToAdopt()/readAgentMediatorDid() trust the mediatorDid value stored in the connection object (populated during onboarding from agent replies) and persist it as the wallet's routing address without any additional verification that the value is legitimate or that the operator approved this specific mediator. 🔎 Evidence: 🎯 Attack Scenario: If an attacker can influence the onboarding response/connection store (e.g., a malicious or compromised agent, or a MITM during onboarding before mediator pinning), the advertised mediatorDid gets silently adopted as the wallet's inbox on the next background restart, redirecting all future inbound DIDComm messages (consent requests, approvals) to an attacker-controlled relay.
🟡 getWalletMediatorDid() return type changed to allow undefined; downstream comparisons may mask null dereference risk
🧠 AI Triage:
📝 Description: getWalletMediatorDid now returns string | undefined instead of a guaranteed string; several callers in offscreen.ts compare this against session mediatorDid or index into maps, and if any caller in the untruncated code assumes a string, incorrect-type usage could occur. 🌱 Root Cause: Change from required field with hardcoded default to optional field removes the previous guarantee that a mediator DID string is always present, and the diff shows the caching wrapper walletMediatorDid() was also changed to always fetch fresh but the full replacement body is truncated in the provided diff, leaving verification of correct undefined-handling incomplete in the visible code. 🔎 Evidence: 🎯 Attack Scenario: Not directly attacker-triggerable from the visible code; this is a code-quality/robustness concern rather than a confirmed exploitable vulnerability given the code shown explicitly handles the undefined case (isInbox: s.mediatorDid === inbox, and the inbox.unset diagnostic check).
|
| Field | Detail |
|---|---|
| Severity | LOW |
| Location | packages/extension/src/offscreen.ts:635 |
| Finding ID | github_pr-02c51798dec7 |
| 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:1243, offscreen.ts:1252
📝 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 cites offscreen.ts line 635 with an empty code_snippet ('code_snippet":""') and no offscreen.ts content was provided in source_files to inspect the actual string-concatenation/format call. EVIDENCE NOT FOUND: offscreen.ts source was not included among source_files, so the alleged util.format/console.log call with non-literal format string at line 635 could not be located, quoted, or verified as reachable from attacker-controlled input. CHANGED VS PRE-EXISTING: offscre
- 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 #148
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | fix/inbox-mediator-from-agent → 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
Removes a hardcoded fallback DIDComm mediator DID (a third-party demo relay) that previously became the default inbox for every wallet whose operator never manually configured routing. Replaces it with a mediator adoption mechanism that reads the wallet's already-onboarded agent's advertised mediator (from local connection state or fresh onboarding replies) and adopts it only when no inbox is currently configured, plus new diagnostics/messaging to surface and recover from an unset inbox.
Diff: +245 / -45 lines
Types: security, bugfix, feature, config
📁 File Classifications
packages/extension/src/config.ts
- Type: security
packages/extension/src/active-vta.ts
- Type: security
packages/extension/src/background.ts
- Type: security
packages/extension/src/bridge-protocol.ts
- Type: security
packages/extension/src/holder.ts
- Type: security
packages/extension/src/offscreen.ts
- Type: security
🛡️ STRIDE Threat Model
Identified Threats (11)
🟠 STRIDE-1: Untrusted chrome.storage.local Data Poisoning in parseAgentMediatorDid
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Elevation of Privilege |
| Severity | High |
| Likelihood | Likely |
| CVSS | 8.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | High |
| CWE | CWE-345,CWE-494,CWE-829 |
| CAPEC | CAPEC-176,CAPEC-441 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: chrome.storage.local read of pnm-connection/v3 in active-vta.ts allows inbox mediator hijack due to unauthenticated trust in a value written by any extension-privileged context, resulting in silent redirection of the wallet's inbound DIDComm channel to an attacker-controlled relay
Evidence: packages/extension/src/active-vta.ts:23-46
const active = conns?.activeVtaDid ? vtas[conns.activeVtaDid]?.mediatorDid : undefined;
if (typeof active === "string" && active) return active;
Attack Scenario:
- Attacker achieves code execution in the extension's privileged context (e.g. via a compromised dependency, a malicious content script exploiting a bridge-protocol message handler, or a supply-chain-compromised npm package bundled into background.ts).
- Attacker writes a crafted
pnm-connection/v3JSON blob intochrome.storage.localcontainingstate.connections.vtas[<did>].mediatorDid = 'did:webvh:attacker-controlled-mediator'and setsactiveVtaDidto that DID. - On next extension boot,
startInboundListener()in background.ts callsreadAgentMediatorDid()(active-vta.ts) which reads and JSON.parses the tamperedpnm-connection/v3value viaparseAgentMediatorDid(). parseAgentMediatorDid()returns the attacker's mediator DID becauseconns?.activeVtaDidmatches the injected VTA entry (lines:const active = conns?.activeVtaDid ? vtas[conns.activeVtaDid]?.mediatorDid : undefined;).inboxToAdopt((await getSettings()).mediatorDid, attackerMediatorDid)in config.ts returns the attacker value becausecurrent(mediatorDid) is unset (if (current) return undefined; return advertised;).setSettings({ mediatorDid: adopt })persists the attacker-controlled mediator as the wallet's permanent inbox, and all future RP/executorconfirmandtask-consent/requestDIDComm messages route through the attacker's relay.
🔎 Threat Clue: Derived from COMP-004 via EP-006, EP-007
- Data Flows: pnm-connection/v3 -> parseAgentMediatorDid -> inboxToAdopt -> setSettings
Preconditions: Attacker has some form of write access to chrome.storage.local (compromised dependency, malicious extension update, prior XSS/message-passing bug, or physical/local access to profile storage), Wallet's mediatorDid setting is currently unset (fresh install or pre-fix installs after this PR ships)
Existing Controls: inboxToAdopt() declines to overwrite an already-set mediatorDid • try/catch around JSON.parse in parseAgentMediatorDid() prevents crash on malformed input
Recommended Mitigations: Cryptographically sign or MAC the pnm-connection/v3 record so tampering is detectable • Validate that any adopted mediatorDid corresponds to a mediator actually reachable via the DIDComm handshake before persisting it • Require explicit user confirmation before ANY automatic mediator adoption, not just on override • Add integrity checks (e.g. HMAC over stored connection state) verified before consumption
🟡 STRIDE-2: Unauthenticated Cross-Context Message Triggering Inbox Restart via RUNTIME_RESTART_INBOX
| Field | Detail |
|---|---|
| Category | Spoofing, Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-306,CWE-862 |
| CAPEC | CAPEC-148 |
| OWASP | A01:2021 - Broken Access Control |
Description: chrome.runtime.onMessage handler for RUNTIME_RESTART_INBOX in background.ts allows unauthenticated resource-exhaustion or session-disruption due to missing sender validation, resulting in denial of inbound DIDComm connectivity or connection churn
Attack Scenario:
- Any extension page or context capable of sending chrome.runtime messages to the background service worker (e.g. a compromised content script that has obtained the extension's messaging channel, or another installed extension with
externally_connectableaccess) sends{ type: RUNTIME_RESTART_INBOX }. - background.ts's
chrome.runtime.onMessage.addListenermatches on(message as { type?: string })?.type === RUNTIME_RESTART_INBOXwith nosender.idor origin verification visible in the shown code. startInboundListener()is invoked repeatedly, tearing down and re-establishing all DIDComm mediator sessions for every known VTA DID.- Repeated invocation from a malicious sender causes connection churn, transient DoS of inbound consent delivery, and re-triggers the inbox backfill logic (
inboxToAdopt) on every call, amplifying STRIDE-1's exploitability window. - Legitimate RP/executor confirm requests arriving during the churn window are dropped or delayed, degrading step-up/consent availability.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: chrome.runtime.onMessage -> startInboundListener
Preconditions: Attacker controls a context able to send chrome.runtime messages to this extension's background worker (malicious/compromised extension, injected content script, or a rogue page if externally_connectable is misconfigured)
Existing Controls: Chrome's extension messaging model restricts message senders to the extension's own contexts unless externally_connectable is configured (not verifiable from provided code)
Recommended Mitigations: Add explicit sender.id === chrome.runtime.id verification in the onMessage listener before acting on RUNTIME_RESTART_INBOX • Rate-limit or debounce startInboundListener() invocations • Restrict externally_connectable in manifest.json to no external IDs unless explicitly required
🟡 STRIDE-3: Removal of Hardcoded Default Mediator Creates Silent Unreachable-Wallet State
| Field | Detail |
|---|---|
| Category | Denial of Service, Repudiation |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.3 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-280,CWE-756 |
| CAPEC | CAPEC-664 |
| OWASP | A04:2021 - Insecure Design |
Description: getWalletMediatorDid in holder.ts allows a fail-open UX of apparent success due to the mediatorDid field silently becoming undefined without forced re-authentication or blocking UI, resulting in consent requests and step-up approvals never being delivered while the user believes routing works
Attack Scenario:
- A wallet upgraded from a pre-fix version (which relied on DEFAULT_WALLET_MEDIATOR_DID) has its config.ts getSettings() logic changed so the default is no longer injected (
...(s?.mediatorDid ? { mediatorDid: s.mediatorDid } : {})). - If the agent never advertised a mediator and no prior connection recorded one,
readAgentMediatorDid()returns undefined andinboxToAdoptalso returns undefined, leavingmediatorDidpermanently unset. getWalletMediatorDid()in holder.ts now legitimately returnsundefinedinstead of throwing or falling back.- Any RP-initiated
confirmrequest ortask-consent/requestDIDComm message sent to this wallet's (nonexistent) inbox silently never arrives, with no proactive alert to the user beyond the passiverunDiagnosticsself-test (which the user must manually trigger). - An attacker who can induce or observe this state (e.g. a malicious RP aware the wallet has no reachable inbox) exploits the resulting approval blackhole to claim consent was never requested, or to force fallback to a less secure interactive-only flow, undermining non-repudiation of the consent process.
🔎 Threat Clue: Derived from COMP-006 via EP-008
- Data Flows: getSettings -> getWalletMediatorDid -> transportHealthSnapshot/runDiagnostics
Preconditions: Wallet onboarded to an agent that advertises no DIDComm mediator (REST/TSP-only) or upgraded from a pre-fix build with no persisted connection record, User does not proactively run the diagnostics self-test
Existing Controls: runDiagnostics() adds an explicit 'inbox.unset' fail check with remediation guidance • Options/Setup UI intended to surface configuration state (not fully verifiable from provided files)
Recommended Mitigations: Surface a persistent, non-dismissible UI banner when mediatorDid is unset rather than requiring a manual diagnostics run • Block or clearly gate step-up/consent-dependent actions when inbox is unset • Emit a background alarm/notification proactively rather than relying on user-triggered self-test
🟡 STRIDE-4: Automatic Silent Inbox Backfill Without User Consent on Every Boot
| Field | Detail |
|---|---|
| Category | Tampering, Repudiation, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Likely |
| 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-778,CWE-284 |
| CAPEC | CAPEC-268 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: startInboundListener in background.ts allows unauthorized persistent configuration mutation due to unconditional automatic setSettings writes derived from stored connection state without explicit user approval, resulting in the wallet's inbox routing changing without an auditable user-driven action
Attack Scenario:
- background.ts's
startInboundListener()runs on every extension boot (browser restart, extension reload, service-worker wake). - It unconditionally calls
inboxToAdopt((await getSettings()).mediatorDid, await readAgentMediatorDid())and, if a value is returned, callssetSettings({ mediatorDid: adopt })with only aconsole.infolog — no user-facing confirmation, no write to any tamper-evident audit trail. - Because the mediatorDid comes from the potentially attacker-influenced
pnm-connection/v3store (see STRIDE-1) or from any agent the wallet has connected to, the wallet's inbox can change automatically and invisibly to the user across restarts. - Should the operator later dispute why messages were routed through an unexpected relay, there is no durable, user-visible log entry beyond an ephemeral console message (lost once devtools closes), preventing after-the-fact verification of when/why the setting changed.
- This creates a repudiation gap: an operator cannot prove the mediator change was attacker-driven vs. legitimate onboarding backfill.
🔎 Threat Clue: Derived from COMP-001 via EP-003
- Data Flows: startInboundListener -> inboxToAdopt -> setSettings
Preconditions: Wallet has a persisted connection record with a mediatorDid value (legitimate or attacker-injected), mediatorDid setting is currently unset
Existing Controls: inboxToAdopt() only fires when current setting is unset, limiting blast radius to a one-time event per wallet • console.info log line exists for local debugging
Recommended Mitigations: Persist a durable, queryable audit log entry (not just console output) whenever mediatorDid is programmatically changed • Prompt for explicit user confirmation before the very first automatic adoption, mirroring the confirmation required for manual changes • Expose the backfill event in the Options/Setup UI history
🟡 STRIDE-5: Type Contract Change on getWalletMediatorDid Enables Downstream Null-Dereference or Fail-Open Consumers
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| 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-476,CWE-704 |
| CAPEC | CAPEC-153 |
| OWASP | A04:2021 - Insecure Design |
Description: getWalletMediatorDid in holder.ts allows inconsistent undefined-handling across callers due to a breaking return-type change from Promise to Promise<string | undefined> without a full audited call-site sweep, resulting in potential runtime exceptions or silent security-relevant fail-open behavior in unreviewed consumers
Attack Scenario:
getWalletMediatorDid()in holder.ts changes its declared return type fromPromise<string>toPromise<string | undefined>.- Any caller written against the old contract that does
const mediator = await getWalletMediatorDid(); doSomething(mediator.toString())or otherwise assumes a non-null string will now throw a TypeError or silently passundefinedinto a security-relevant comparison (e.g.s.mediatorDid === inboxin transportHealthSnapshot). - Provided diff shows offscreen.ts's
transportHealthSnapshot()andrunDiagnostics()were updated correctly, but the recon notes explicitly flag that a full call-site audit was not possible with the reduced file set. - An unreviewed or third-party-contributed call site that silently treats
undefinedas falsy-but-valid could mark a sessionisInbox: falsewhen it should be true (or vice versa), causing the self-test or health snapshot to misreport wallet reachability. - An attacker exploiting the resulting misreport could induce a user to believe the wallet inbox is broken (masking a legitimate attack window) or believe it is healthy when it is not (masking STRIDE-3's DoS state).
🔎 Threat Clue: Derived from COMP-006 via EP-008
- Data Flows: getSettings -> getWalletMediatorDid -> (unaudited callers)
Preconditions: Existence of an unaudited call site to getWalletMediatorDid() outside the files provided in this PR's diff, TypeScript strict null checks not enforced project-wide, or call site uses non-null assertion (!) bypassing the compiler
Existing Controls: TypeScript static typing would catch most misuse at compile time if strictNullChecks is enabled project-wide • Diff shows two of the known call sites (offscreen.ts) were correctly updated
Recommended Mitigations: Run a full-repository grep/compile check for all getWalletMediatorDid() call sites before merge • Add an explicit runtime assertion/logging wrapper around the function during a migration window • Add unit tests specifically asserting undefined-handling at every consumer
🟡 STRIDE-6: Missing Mediator Reachability Validation Before Adoption Enables Blind Trust in Advertised DID
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-295,CWE-345 |
| CAPEC | CAPEC-94 |
| OWASP | A07:2021 - Identification and Authentication Failures |
Description: inboxToAdopt in config.ts allows adoption of an unvalidated relay identifier due to lack of cryptographic or connectivity verification prior to persisting it as the wallet inbox, resulting in a malicious or compromised onboarding agent silently becoming the wallet's permanent message relay
Attack Scenario:
- During onboarding (
doOnboardConnectin offscreen.ts), the wallet contacts an agent and receivesservices.didcomm?.mediatorDidas the agent's advertised mediator. - A malicious or compromised onboarding agent (e.g. a phished admin endpoint, DNS-hijacked agent host, or MITM'd initial handshake) advertises an attacker-controlled mediator DID in its service descriptor.
inboxToAdopt((await getSettings()).mediatorDid, services.didcomm?.mediatorDid)returns the attacker's DID because the current setting is unset (fresh onboarding).setSettings({ mediatorDid: inbox })persists the attacker's relay as the wallet's permanent inbox with no verification that the mediator DID resolves to a legitimate, reachable, or expected DIDComm service.- All future inbound consent/step-up requests targeting this wallet are routed through the attacker's relay, enabling interception, replay, or selective dropping of RP-initiated confirm requests — undermining the integrity of the step-up authorization flow.
🔎 Threat Clue: Derived from COMP-002 via EP-004
- Data Flows: doOnboardConnect -> inboxToAdopt -> setSettings
Preconditions: Attacker can control or MITM the onboarding agent's advertised service descriptor (compromised agent infra, DNS hijack, or malicious agent operator), Onboarding occurs over a channel without independent mediator verification
Existing Controls: Onboarding presumably occurs over an authenticated admin channel to a known agent DID (not fully verifiable from provided files)
Recommended Mitigations: Perform a DIDComm handshake/ping to the advertised mediator before persisting it as the inbox to confirm liveness and correct protocol behavior • Pin or verify the agent's DID document signature over its service descriptor before trusting the mediatorDid field • Surface the adopted mediator DID to the user for explicit confirmation during onboarding rather than fully automatic adoption
🔵 STRIDE-7: Race Condition Between Concurrent startInboundListener Invocations Causing Inconsistent Inbox State
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 4.0 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: startInboundListener in background.ts allows a TOCTOU race due to concurrent boot-time and message-triggered invocations both reading and writing mediatorDid without locking, resulting in inconsistent or overwritten inbox configuration
Attack Scenario:
- Extension boot triggers
startInboundListener()automatically, beginning agetSettings()→readAgentMediatorDid()→setSettings()sequence. - Before this sequence completes (all async), the newly added
RUNTIME_RESTART_INBOXmessage handler (STRIDE-2) is triggered — either legitimately (user changed the mediator by hand in Setup) or maliciously — invokingstartInboundListener()a second time concurrently. - Both invocations independently call
getSettings(), observemediatorDidas unset (before the first write completes), and both computeinboxToAdoptresults based on stale reads. - Both then call
setSettings({ mediatorDid: adopt }); whichever write lands last silently overwrites the other, potentially discarding a legitimate manual selection made concurrently in the Setup UI or a legitimate first-adoption in favor of a stale second read. - The wallet ends up with an inbox value inconsistent with what any single caller intended, and the two competing inbound listener setups may leave duplicate or half-closed mediator sessions.
🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-003
- Data Flows: startInboundListener (concurrent invocations) -> setSettings
Preconditions: Two invocations of startInboundListener() (boot + RUNTIME_RESTART_INBOX, or two rapid RUNTIME_RESTART_INBOX calls) overlap in time, No mutex/lock around the read-decide-write settings sequence
Existing Controls: inboxToAdopt requires current to be unset, narrowing the race window to first-adoption scenarios only
Recommended Mitigations: Add a mutex/lock (e.g. a single in-flight promise guard) around startInboundListener() to serialize invocations • Use optimistic concurrency control (e.g. compare-and-swap semantics) when writing settings • Debounce RUNTIME_RESTART_INBOX handling
🔵 STRIDE-8: Prototype/Structure Pollution via Unvalidated JSON.parse of pnm-connection/v3
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| 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-1284 |
| CAPEC | CAPEC-153 |
| OWASP | A03:2021 - Injection |
Description: parseAgentMediatorDid in active-vta.ts allows malformed deeply-nested object traversal due to weak structural validation of parsed JSON before use in a for...of Object.values loop, resulting in potential type-confusion or unexpected iteration behavior on crafted storage payloads
Attack Scenario:
- Attacker with storage write access (see STRIDE-1 precondition) sets
pnm-connection/v3to a JSON string whosestate.connections.vtasvalue is an object with a crafted__proto__or extremely large key set. parseAgentMediatorDid()casts the parsed result via an unchecked TypeScript type assertion (as { state?: {...} }) with no runtime schema validation (e.g. no zod/ajv validation), trusting the shape entirely.Object.values(vtas)iterates over all enumerable properties, and a maliciously large object (thousands of keys) could cause a measurable performance delay on every extension boot, since this runs insidestartInboundListener().- While a full prototype-pollution sink was not observed in the reduced code, the pattern of unchecked type assertions over externally-influenced JSON without a runtime validator is a latent risk if any future code path merges this parsed object into another structure (e.g. Object.assign) without hardening.
- Repeated boots or extension reloads with a maliciously bloated storage record degrade extension responsiveness, especially on resource constrained devices.
🔎 Threat Clue: Derived from COMP-004 via EP-006, EP-007
- Data Flows: chrome.storage.local.get(pnm-connection/v3) -> parseAgentMediatorDid
Preconditions: Attacker has storage write access as in STRIDE-1, No runtime schema validation library is used on the parsed structure
Existing Controls: try/catch guards against JSON.parse throwing • Only mediatorDid string values are extracted and used downstream, limiting object-injection sinks in the shown code
Recommended Mitigations: Adopt a runtime schema validator (zod/ajv) for the pnm-connection/v3 structure before use • Cap the number of vtas entries iterated or add resource limits • Avoid unchecked as type assertions on data crossing a trust boundary
🔵 STRIDE-9: Insufficient Logging of Mediator Adoption Events Undermines Forensic Traceability
| 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 |
| CAPEC | CAPEC-268 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: setSettings mediator writes in background.ts and offscreen.ts allow untraceable inbox changes due to console.info being the only recorded evidence of an automatic mediator adoption, resulting in an inability to reconstruct when or why the wallet's inbox relay changed during incident response
Attack Scenario:
- An automatic mediator adoption occurs either at boot (
startInboundListener) or during onboarding (doOnboardConnect), both logging only viaconsole.info("[pnm inbound] inbox mediator backfilled from agent:", adopt)orconsole.info("[pnm onboard] inbox mediator set from agent:", inbox). - Chrome extension service-worker console output is ephemeral — it is only visible while DevTools is attached and is lost on service worker termination/idle suspension, which Chrome does aggressively for MV3 background workers.
- Weeks later, a user or incident responder notices consent requests silently failing to arrive and suspects the inbox mediator was changed to an unexpected relay (possibly maliciously, per STRIDE-1).
- There is no durable, queryable audit trail (e.g. a persisted change-log entry with timestamp and reason) to determine when the change happened, whether it was the legitimate onboarding backfill or an attacker-driven overwrite.
- The incident cannot be conclusively attributed, delaying remediation and potentially allowing an attacker-controlled relay to remain trusted indefinitely.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-003, EP-004
- Data Flows: startInboundListener/doOnboardConnect -> setSettings -> console.info
Preconditions: An automatic mediator adoption event has occurred, No external log aggregation of extension console output is configured
Existing Controls: console.info calls exist at both adoption sites, aiding live debugging only
Recommended Mitigations: Persist a durable audit record (timestamp, previous value, new value, trigger source) in chrome.storage.local on every mediatorDid change • Surface the change history in the Options/Setup UI for user review • Consider forwarding security-relevant config changes to an optional telemetry/audit endpoint with user consent
⚪ STRIDE-10: Removed Memoization Introduces Repeated Storage Reads Enabling Timing-Based Information Disclosure of Inbox State
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Informational |
| Likelihood | Very Unlikely |
| CVSS | 1.8 CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-208 |
| CAPEC | CAPEC-462 |
| OWASP | A04:2021 - Insecure Design |
Description: walletMediatorDid in offscreen.ts allows minor timing variance disclosure due to removal of the _walletMediatorDid cache in favor of a fresh getSettings() read on every call, resulting in a theoretical side channel revealing whether the mediatorDid setting is populated based on response latency
Attack Scenario:
- The diff removes the module-level
_walletMediatorDidcache and readsgetSettings()fresh on every call towalletMediatorDid(). getSettings()performs an IndexedDB/chrome.storage read on every invocation rather than returning a cached, already-resolved string.- A co-located malicious extension or script with high-resolution timing access (e.g. via a shared side-channel API deprecated in most modern browsers) could theoretically distinguish between a fast in-memory path and a slower storage-backed path across repeated calls to functions that internally invoke
walletMediatorDid()(e.g.transportHealthSnapshot,runDiagnostics). - This timing variance could, in a highly constrained threat model, leak whether the mediatorDid field is populated (extra branch/parsing work) versus unset, though the practical exploitability is minimal given browser isolation of extension storage APIs and Chrome's mitigations against high-resolution timing side channels.
- No direct data exfiltration path exists; this is a purely theoretical, low-confidence timing side channel included per exhaustive analysis requirements.
🔎 Threat Clue: Derived from COMP-006 via EP-008
- Data Flows: walletMediatorDid -> getSettings
Preconditions: Attacker has a co-located execution context with high-resolution timing capability, Modern browser timing-attack mitigations (jittered timers, isolated storage) are bypassed or ineffective
Existing Controls: Chrome's process isolation between extensions • Browser-level timer resolution reduction mitigations
Recommended Mitigations: No action required given negligible practical risk; optionally reintroduce a short-lived cache with explicit invalidation on settings change if performance regressions are observed
🔵 STRIDE-11: Lack of Rate Limiting on RUNTIME_RESTART_INBOX Enables Resource Exhaustion of Mediator Session Pool
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.7 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-770,CWE-400 |
| CAPEC | CAPEC-125 |
| OWASP | A04:2021 - Insecure Design |
Description: RUNTIME_RESTART_INBOX handler in background.ts allows repeated session churn due to absence of rate limiting or cooldown on startInboundListener invocations, resulting in resource exhaustion of network sockets/mediator sessions and degraded extension responsiveness
Attack Scenario:
- A local malicious actor (compromised sibling extension, malicious page abusing externally_connectable, or automated test harness left enabled in production) repeatedly sends
RUNTIME_RESTART_INBOXmessages in a tight loop. - Each message triggers a full
startInboundListener()cycle: closing existing mediator WebSocket/DIDComm sessions and re-establishing new ones for every known VTA DID. - Without any cooldown, this creates a thundering-herd effect against the wallet's configured mediator(s), consuming file descriptors/sockets and potentially triggering rate limiting or IP bans from the legitimate mediator service.
- The wallet's genuine inbound message delivery is degraded or fails during the churn, effectively achieving the same consent-blackout outcome as STRIDE-3 but through active exploitation rather than passive misconfiguration.
- If the mediator service itself rate-limits or blocks the wallet's connection attempts as abusive, the wallet may be locked out of that mediator entirely, requiring manual intervention to restore service.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: RUNTIME_RESTART_INBOX -> startInboundListener (repeated)
Preconditions: Attacker can send repeated RUNTIME_RESTART_INBOX messages (see STRIDE-2 precondition), No debounce/cooldown logic exists in the handler
Existing Controls: None observed in the provided diff beyond the basic message-type match
Recommended Mitigations: Add a cooldown/debounce timer (e.g. minimum 5-10s between restarts) around startInboundListener() invocations triggered by this message • Log and alert on abnormally frequent restart requests • Combine with sender validation from STRIDE-2 to eliminate the untrusted-sender vector entirely
🍝 PASTA Threat Model
Application Purpose
A Chrome browser extension implementing a decentralized-identity (DIDComm/DID) wallet that lets a user hold verifiable credentials, connect to relying parties (RPs) and executor agents, and approve step-up authorization/consent requests routed through a configurable DIDComm mediator relay.
Inherent Risks
- The wallet's reachability for consent/authorization requests depends entirely on a single configurable mediator relay setting.
- Chrome extension service workers are ephemeral and their console logging is not a durable audit trail.
- Cross-context message passing within the extension is a recurring trust-boundary surface for spoofed or replayed messages.
Objectives
Risk: Treat any automatic mutation of security-relevant settings (inbox mediator) as a high-risk operation requiring strong provenance.; Treat unset/undefined security configuration as a safe default that fails closed with clear user signaling, not a silent failure.
Business: Provide users a trustworthy, self-sovereign identity wallet for interacting with relying parties and AI/automation executors.; Maintain user confidence that consent and authorization requests are reliably delivered and cannot be silently redirected.
Security: Ensure only legitimate, verified agents can influence which mediator relay carries the wallet's inbound messages.; Prevent unauthenticated or unauthorized contexts from triggering privileged background operations.; Provide durable, tamper-evident audit trails for security-relevant configuration changes.
Financial: Avoid liability from unauthorized transactions or approvals resulting from a hijacked or unreachable wallet inbox.; Minimize support costs from users experiencing silent consent-delivery failures.
Compliance: Support user auditability of consent request handling for accountability in RP-relying scenarios.; Avoid undisclosed routing of user data (DIDComm messages) through unvetted third-party infrastructure.
Functional: Allow the wallet to automatically adopt a sensible inbox mediator from onboarding without requiring manual configuration for the common case.; Support multi-relay/advanced deployments where an operator manually configures the mediator.
Operational: Ensure inbound DIDComm sessions are re-established reliably after settings changes without requiring a full browser restart.; Keep the inbox mediator configuration state consistent across extension boots and onboarding events.
Business Impact Analysis (3)
BIA-1: Wallet Inbound Consent Delivery (Critical)
The end-to-end process by which relying parties and executor agents push confirm/consent requests through the wallet's configured mediator to reach the user for approval.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Extension Users / Onboarding Agents / Relying Parties / Wallet Operators
- Dependencies: DIDComm Mediator Relay / chrome.storage.local Connection Store / Wallet Settings Store (IndexedDB) / Background Service Worker
- Disruptions: Inbox mediator hijacked to an attacker-controlled relay / Inbox mediator setting silently left unset after upgrade / Repeated RUNTIME_RESTART_INBOX calls causing session churn/DoS
- Impacts: Users miss time-sensitive consent/step-up approvals, blocking legitimate transactions / Attacker-controlled relay can intercept, delay, or selectively drop RP confirm requests / Reputational damage if users discover their wallet routed traffic through an undisclosed relay
BIA-2: Wallet Onboarding and Mediator Adoption (High)
The process by which a wallet connects to an agent for the first time, mints/loads a holder identity, and adopts the agent's advertised DIDComm mediator as its inbox.
MTD: 03 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Extension Users / Onboarding Agents / Wallet Operators
- Dependencies: Onboarding Admin Channel / Agent Service Descriptor (services.didcomm) / Wallet Settings Store
- Disruptions: Malicious or compromised agent advertises an attacker-controlled mediator during onboarding / Concurrent onboarding and boot-time backfill race writing inconsistent settings
- Impacts: Wallet inbox permanently pointed at an untrusted relay from first use / Inconsistent configuration state requiring manual remediation
BIA-3: Cross-Context Runtime Messaging (Medium)
The internal message-passing mechanism by which popup, options, offscreen, and background contexts of the extension coordinate privileged operations such as restarting inbound listeners.
MTD: 07 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Extension Users / Extension Codebase Maintainers
- Dependencies: chrome.runtime.onMessage API / Background Service Worker
- Disruptions: Unauthenticated sender triggers privileged RUNTIME_RESTART_INBOX repeatedly / Malicious sibling extension abuses shared messaging surface
- Impacts: Session churn degrading inbound message delivery / Resource exhaustion of mediator connections
Technical Scope
Roles (3): RO-1 Wallet Operator/User · RO-2 Onboarding Agent Administrator · RO-3 Extension Background Process
Actors (3): AC-1 Extension User · AC-2 Background Service Worker Process · AC-3 Onboarding Agent Service
Entry Points (5): EP-001 Restart Inbox Runtime Message · EP-003 Extension Boot Hook · EP-004 Onboarding Connect Flow · EP-006 Agent Mediator Reader · EP-008 Wallet Mediator Getter
Threat Actors (3): TA-1 Malicious/Compromised Onboarding Agent Operator · TA-2 Local Malicious Extension/Script · TA-3 Supply Chain Attacker
Infrastructure (2): IF-1 User's Local Browser Profile · IF-2 DIDComm Mediator Hosting
Trust Boundaries (3): TB-1 Browser Extension Privileged Contexts · TB-2 External DIDComm Network · TB-3 Sibling Browser Extensions / Web Pages
External Entities (3): EE-1 Relying Party (RP) · EE-2 Executor Agent · EE-3 DIDComm Mediator Operator
System Components (7): SC-1 Background Service Worker · SC-2 Offscreen Document (Onboarding/Diagnostics) · SC-3 Wallet Settings Store · SC-4 Connection Store (pnm-connection/v3) · SC-5 DIDComm Mediator Relay · SC-6 Onboarding Agent · SC-7 Options/Setup UI
Resources And Assets (3): RA-1 Wallet Inbox Mediator DID Setting · RA-2 Connection State per VTA · RA-3 Holder Identity (did:key)
Technologies And Dependencies (4): TD-1 @openvtc/pnm-core (IndexedDBKVStore) · TD-2 @openvtc/vti-didcomm-js · TD-3 chrome.storage / chrome.runtime APIs · TD-4 React
Use Cases (3)
- Onboarding to an Agent and Establishing Inbox: A wallet user connects to a DIDComm agent for the first time; the extension mints/loads the holder identity and adopts the agent's advertised mediator as the wallet's inbox if none is already set.
- Receiving an Inbound Consent Request: A relying party pushes a confirm/consent request through the wallet's configured mediator; the background service worker's inbound listener receives it and surfaces it to the user for approval.
- Manually Reconfiguring the Inbox Mediator: A wallet operator running a multi-relay deployment manually sets a new mediator DID in the Setup/Options UI and triggers a restart of the inbound listener so the change takes effect immediately.
📋 Risk Registry (5)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Wallet inbox mediator can be hijacked via tampered chrome.storage.local connection record, silently redirecting all inbound consent traffic to an attacker-controlled relay | High | High | Immediate | Medium |
| RISK-002 | Unauthenticated cross-context message can trigger repeated privileged inbound listener restarts, enabling session churn and DoS of consent delivery | Medium | Medium | Short-Term | Low |
| RISK-003 | Malicious or compromised onboarding agent can have its advertised mediator blindly adopted as the wallet's permanent inbox without reachability or identity verification | Medium | Medium | Short-Term | Medium |
| RISK-004 | Silent unset-inbox state after upgrade or REST/TSP-only onboarding leaves users unaware their wallet cannot receive consent requests | Medium | Low | Medium-Term | Low |
| RISK-005 | Automatic settings mutation on every boot lacks durable audit trail, undermining forensic attribution of inbox changes | Low | Low | Medium-Term | Low |
⚔️ Attack Scenarios (3)
SC-4: Connection Store (pnm-connection/v3)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Local Malicious Extension/Script<br><i>Disrupt or hijack the wallet's background process</i>" }
TA3@{ shape: rect, label: "TA-3: Supply Chain Attacker<br><i>Inject malicious logic to manipulate stored state</i>" }
end
subgraph SL2["2. Threats"]
direction LR
T1@{ shape: rect, label: "STRIDE-1: Untrusted chrome.storage.local Data Poisoning<br><i>High / Likely</i>" }
T2@{ shape: rect, label: "STRIDE-8: Unvalidated JSON.parse Traversal<br><i>Low / Unlikely</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC176@{ shape: rect, label: "CAPEC-176: Configuration/Environment Manipulation" }
CAPEC441@{ shape: rect, label: "CAPEC-441: Malicious Logic Insertion" }
CAPEC153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
end
subgraph SL4["4. Weaknesses"]
direction LR
CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
CWE494@{ shape: rect, label: "CWE-494: Download of Code Without Integrity Check" }
CWE20@{ shape: rect, label: "CWE-20: Improper Input Validation" }
end
subgraph SL5["5. System Component"]
direction LR
SC4@{ shape: rect, label: "SC-4: Connection Store (pnm-connection/v3)" }
end
TA2 --> T1
TA3 --> T1
T1 --> CAPEC176
T1 --> CAPEC441
CAPEC176 --> CWE345
CAPEC441 --> CWE494
CWE345 --> SC4
CWE494 --> SC4
TA2 --> T2
T2 --> CAPEC153
CAPEC153 --> CWE20
CWE20 --> SC4
linkStyle 0 stroke:#FF0000,stroke-width:2px
linkStyle 1 stroke:#FF0000,stroke-width:2px
linkStyle 2 stroke:#FF0000,stroke-width:2px
linkStyle 3 stroke:#FF0000,stroke-width:2px
linkStyle 4 stroke:#FF0000,stroke-width:2px
linkStyle 5 stroke:#FF0000,stroke-width:2px
linkStyle 6 stroke:#FF0000,stroke-width:2px
linkStyle 7 stroke:#FF0000,stroke-width:2px
linkStyle 8 stroke:#00FF00,stroke-width:2px
linkStyle 9 stroke:#00FF00,stroke-width:2px
linkStyle 10 stroke:#00FF00,stroke-width:2px
linkStyle 11 stroke:#00FF00,stroke-width:2px
SC-1: Background Service Worker
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Local Malicious Extension/Script<br><i>Disrupt via unauthenticated messaging</i>" }
end
subgraph SL2["2. Threats"]
direction LR
T1@{ shape: rect, label: "STRIDE-2: Unauthenticated RUNTIME_RESTART_INBOX<br><i>Medium / Possible</i>" }
T2@{ shape: rect, label: "STRIDE-7: Race Condition on Concurrent Restarts<br><i>Low / Possible</i>" }
T3@{ shape: rect, label: "STRIDE-11: No Rate Limiting on Restart<br><i>Low / Possible</i>" }
T4@{ shape: rect, label: "STRIDE-4: Silent Automatic Inbox Backfill<br><i>Medium / Likely</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC148@{ shape: rect, label: "CAPEC-148: Content Spoofing via Message" }
CAPEC26@{ shape: rect, label: "CAPEC-26: Leveraging Race Conditions" }
CAPEC125@{ shape: rect, label: "CAPEC-125: Flooding" }
CAPEC268@{ shape: rect, label: "CAPEC-268: Audit Log Manipulation" }
end
subgraph SL4["4. Weaknesses"]
direction LR
CWE306@{ shape: rect, label: "CWE-306: Missing Authentication for Critical Function" }
CWE362@{ shape: rect, label: "CWE-362: Concurrent Execution using Shared Resource" }
CWE770@{ shape: rect, label: "CWE-770: Allocation of Resources Without Limits" }
CWE778@{ shape: rect, label: "CWE-778: Insufficient Logging" }
end
subgraph SL5["5. System Component"]
direction LR
SC1@{ shape: rect, label: "SC-1: Background Service Worker" }
end
TA2 --> T1
T1 --> CAPEC148
CAPEC148 --> CWE306
CWE306 --> SC1
TA2 --> T2
T2 --> CAPEC26
CAPEC26 --> CWE362
CWE362 --> SC1
TA2 --> T3
T3 --> CAPEC125
CAPEC125 --> CWE770
CWE770 --> SC1
T4 --> CAPEC268
CAPEC268 --> CWE778
CWE778 --> SC1
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#FFA500,stroke-width:2px
linkStyle 2 stroke:#FFA500,stroke-width:2px
linkStyle 3 stroke:#FFA500,stroke-width:2px
linkStyle 4 stroke:#00FF00,stroke-width:2px
linkStyle 5 stroke:#00FF00,stroke-width:2px
linkStyle 6 stroke:#00FF00,stroke-width:2px
linkStyle 7 stroke:#00FF00,stroke-width:2px
linkStyle 8 stroke:#00FF00,stroke-width:2px
linkStyle 9 stroke:#00FF00,stroke-width:2px
linkStyle 10 stroke:#00FF00,stroke-width:2px
linkStyle 11 stroke:#00FF00,stroke-width:2px
linkStyle 12 stroke:#FFA500,stroke-width:2px
linkStyle 13 stroke:#FFA500,stroke-width:2px
linkStyle 14 stroke:#FFA500,stroke-width:2px
SC-2: Offscreen Document (Onboarding/Diagnostics)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious/Compromised Onboarding Agent Operator<br><i>Intercept DIDComm consent traffic</i>" }
end
subgraph SL2["2. Threats"]
direction LR
T1@{ shape: rect, label: "STRIDE-6: Missing Mediator Reachability Validation<br><i>Medium / Possible</i>" }
T2@{ shape: rect, label: "STRIDE-3: Silent Unreachable-Wallet State<br><i>Medium / Likely</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC94@{ shape: rect, label: "CAPEC-94: Adversary in the Middle" }
CAPEC664@{ shape: rect, label: "CAPEC-664: Server Side Request Forgery (Fail-Open Analog)" }
end
subgraph SL4["4. Weaknesses"]
direction LR
CWE295@{ shape: rect, label: "CWE-295: Improper Certificate/Identity Validation" }
CWE280@{ shape: rect, label: "CWE-280: Improper Handling of Insufficient Permissions" }
end
subgraph SL5["5. System Component"]
direction LR
SC2@{ shape: rect, label: "SC-2: Offscreen Document" }
end
TA1 --> T1
T1 --> CAPEC94
CAPEC94 --> CWE295
CWE295 --> SC2
T2 --> CAPEC664
CAPEC664 --> CWE280
CWE280 --> SC2
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#FFA500,stroke-width:2px
linkStyle 2 stroke:#FFA500,stroke-width:2px
linkStyle 3 stroke:#FFA500,stroke-width:2px
linkStyle 4 stroke:#FFA500,stroke-width:2px
linkStyle 5 stroke:#FFA500,stroke-width:2px
📊 Risk Summary
Total Threats: 11
By Severity: Low: 4 · High: 1 · Medium: 5 · Informational: 1
By Category: Spoofing: 3 · Tampering: 6 · Elevation of Privilege: 2 · Denial of Service: 6 · Repudiation: 3 · Information Disclosure: 1
🎯 Attack Surface
Kill Chain 1: An attacker who first gains any write access to chrome.storage.local — via a compromised bundled dependency (supply chain), a co-installed malicious extension, or a prior unrelated extension vulnerability — can inject a crafted pnm-connection/v3 record (STRIDE-1) that parseAgentMediatorDid() trusts without signature or reachability verification; on the next boot, startInboundListener()'s inboxToAdopt() logic silently persists the attacker's mediator DID as the wallet's permanent inbox (STRIDE-4), after which every RP confirm and executor task-consent request is transparently routed through attacker-controlled infrastructure, enabling interception, replay, or selective suppression of authorization prompts — a single storage-write primitive escalating into full compromise of the wallet's inbound authorization channel. Kill Chain 2: Independently, an attacker able to send chrome.runtime messages to the extension's background worker (a malicious sibling extension abusing externally_connectable, or a compromised content script) can repeatedly fire the newly-introduced unauthenticated RUNTIME_RESTART_INBOX message (STRIDE-2) to force startInboundListener() to churn every mediator session; combined with the missing mutex around the read-decide-write settings sequence (STRIDE-7) and absent rate limiting (STRIDE-11), this both degrades legitimate consent delivery and re-opens the backfill race window on every restart, compounding Kill Chain 1's exploitability. Kill Chain 3: A more targeted actor operating or compromising an onboarding agent (TA-1) can advertise an attacker-controlled mediator during the legitimate onboarding handshake; because inboxToAdopt() performs no reachability or identity verification of the advertised mediator (STRIDE-6), and the wallet's own getWalletMediatorDid() now legitimately returns undefined for wallets that never onboard to a DIDComm-capable agent (STRIDE-3, STRIDE-5), a victim may either be silently routed through a hostile relay from first use, or experience a false sense of security from a passing diagnostics run on a stale cached call site that was not audited for the new nullable contract.
🛡️ Risk Mitigation Strategy
Priority 1 (Immediate): Close the trust gap around the pnm-connection/v3 connection store and the automatic inbox-adoption pipeline that consumes it — add integrity verification (signing/MAC) of the stored connection record, require a liveness/handshake check of any mediator DID before it is persisted via inboxToAdopt, and add explicit sender.id validation to the RUNTIME_RESTART_INBOX handler in background.ts so only the extension's own privileged contexts can trigger session restarts; these three changes collectively neutralize Kill Chains 1 and 2 at their root rather than only mitigating symptoms. Priority 2 (Short-Term): Harden the onboarding-time mediator adoption path by surfacing the adopted mediator DID to the user for explicit confirmation and performing a DIDComm handshake before trusting an agent's advertised relay (STRIDE-6), and add a mutex/debounce around startInboundListener() to eliminate the read-decide-write race condition that both legitimate concurrent triggers and attacker-induced restarts can exploit (STRIDE-7, STRIDE-11). Priority 3 (Medium-Term): Improve observability and user-facing signaling of the wallet's own inbox health — replace ephemeral console.info logging of mediator changes with a durable, timestamped audit record queryable from the Options/Setup UI (STRIDE-4, STRIDE-9), and add a persistent, non-dismissible UI indicator (rather than a manually-triggered diagnostics check) whenever mediatorDid is unset so users are not left believing consent delivery is functioning when it structurally cannot be (STRIDE-3). Priority 4 (Long-Term): Conduct a full-repository audit of all call sites consuming the newly-nullable getWalletMediatorDid() return type to eliminate latent null-handling defects (STRIDE-5), and adopt a runtime schema validator (e.g., zod) for all data read from chrome.storage.local that crosses this extension's internal trust boundary, closing the unchecked-type-assertion pattern that currently underlies both
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 2 | 1 |
Confirmed (2)
- 🟡 Inbox mediator auto-adopted from unauthenticated agent-advertised data without integrity/authorization check
- 🟡 getWalletMediatorDid() return type changed to allow undefined; downstream comparisons may mask null dereference risk (triaged LOW→MEDIUM)
Must-Review-By-Human (1)
- 🔵 Unsafe Formatstring (3 occurrences)
What was wrong
packages/extension/src/config.tscarriedsubstituted by
getSettings()whenevermediatorDidwas unset. The only writer of that setting was the advanced "Change routing" field in Setup — onboarding never wrote it. So every wallet whose operator never opened that field ran its inbox through a demo host on a domain no deployment in use here runs, whichever agent it had onboarded to. That relay carries inbound consent requests and approvals.Found from a self-test screenshot: every check green, every host on the operator's own deployment except
inbox mediator → https://mediator.vtc.storm.ws.Setup meanwhile said:
Nothing did it. A claim not backed by evidence (guide §0), covering for the routing it misdescribed.
The fix
Onboarding adopts the agent's advertised DIDComm mediator; the constant is removed, not repointed.
inboxToAdoptholds the rule in one place:Unset is now a real state, and says so
getWalletMediatorDid()returnsstring | undefined.NoInboxMediatorError— codewallet/inbox-not-configured, matched structurally (R3.7).reconcileInboundreturns with one clear line instead of feeding every VTA into a backoff loop. Backoff exists to outlast an outage; retrying cannot fix an unset setting, and would bury the one line saying what is wrong.Migration — this is the part worth reviewing
Wallets onboarded before this have the agent's mediator on their persisted
Connectionbut no setting. Re-onboarding to acquire one mints a fresh holder DID and invalidates every RP ACL, so "just reconnect" is not a fair ask.startInboundListenerbackfills from the connection on every boot — the sameinboxToAdoptrule, at the one place that always runs, declining when an inbox is already set so it never moves an address in use. This mirrorstspMediatorDid, backfilled onto existing connections for the same reason.Net effect for an existing wallet: the inbox moves from the storm.ws demo relay to its own agent's, on the next service-worker start, with no re-onboard.
Also fixed, same blast radius
offscreen.ts, justified as keeping the "is this our inbox?" check synchronous insideonClose— but that check is awaited intoisInboxbefore the closure is built. It bought nothing and went stale whenever settings were written from the options page, a different context. Removed.RUNTIME_RESTART_INBOX). The inbound reconcile otherwise runs only on boot and on a connection-store change, so Setup reported "Saved." over a wallet still listening on the old relay.did:peer:2with the mediator encoded inline. A v4 holder is a VTA-minteddid:keycarrying no mediator, andsaveInboxmints nothing. The scariest warning in Setup guarded a consequence that no longer happens, while deterring people from fixing the routing it was describing.Verification
Lint, build and 683 tests pass.
tests/wallet-inbox.test.mtsadds 10, pinning the adopt rule, the backfill parser (active-agent preference, fallback, REST-only, unreadable storage, non-string field), and that no DID literal with a real identifier body ships insrc— that last one verified to actually fail by reintroducing the old constant, not just asserted to pass.Not verified by me: the live path. Worth confirming after install that the self-test's inbox row names your own mediator.
Pre-merge checklist