Skip to content

fix(inbox): take the wallet's inbox from its agent, not a hardcoded relay - #148

Merged
stormer78 merged 1 commit into
mainfrom
fix/inbox-mediator-from-agent
Aug 31, 2026
Merged

fix(inbox): take the wallet's inbox from its agent, not a hardcoded relay#148
stormer78 merged 1 commit into
mainfrom
fix/inbox-mediator-from-agent

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

What was wrong

packages/extension/src/config.ts carried

DEFAULT_WALLET_MEDIATOR_DID =
  "did:webvh:QmTS3a3H9Dk4ZMPAZ8jNWGeyPbuKrPbrPZcSbg8CJ6yynD:webvh.storm.ws:mediator"

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

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.

The fix

Onboarding adopts the agent's advertised DIDComm mediator; the constant is removed, not repointed. inboxToAdopt holds the rule in one place:

  • Nothing advertised → write nothing. A REST/TSP-only agent cannot push to a wallet, and an inbox invented here is a relay nobody was asked about.
  • Already set → leave it. Either an operator running two relays meant it, or a previous onboarding adopted it — and the inbox is an address others already route to, so a second onboarding silently moving it strands everyone who knows this wallet.
  • Otherwise → adopt the agent's.

Unset is now a real state, and says so

  • getWalletMediatorDid() returns string | undefined.
  • Paths that must open a session throw NoInboxMediatorError — code wallet/inbox-not-configured, matched structurally (R3.7).
  • reconcileInbound returns 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.
  • The self-test fails with "nothing can reach this wallet" — where it previously went on to prove a stranger's relay healthy and call that a pass.

Migration — this is the part worth reviewing

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, so "just reconnect" is not a fair ask. startInboundListener backfills from the connection on every boot — the same inboxToAdopt rule, at the one place that always runs, declining when an inbox is already set so it never moves an address in use. This mirrors tspMediatorDid, 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

  • A stale memo. The inbox DID was cached in offscreen.ts, 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.
  • Changing the inbox by hand now takes effect (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.
  • Setup names the relay it claims to have configured, and flags when it is not the agent's own — a claim about where your messages go should be checkable where it is made.
  • The re-mint warning was stale and load-bearing in the wrong direction. "Changing it mints a brand-new identity… there is no undo" 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. 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.mts adds 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 in src — 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

- [x] No new reqwest::Client::new() / bare fetch(); all clients have finite timeouts (R1.2) — no new fetches
- [x] No lock held across a network await (R1.3) — n/a
- [x] No local state committed before its remote effect, or the flow is resumable with an idempotency key (R2.1) — the inbox write follows a completed onboarding and is idempotent
- [x] Every retry is bounded + backed off; non-idempotent ops are not blind-retried (R1.4) — unchanged
- [x] Accept/poll/listen loops survive transient errors (R1.5) — inbound backoff untouched; the unset-inbox path deliberately does not enter it (config, not outage)
- [x] Acks/deletes happen only after durable handoff (R1.6) — inbound handler untouched
- [x] New/changed wire types: camelCase, deny_unknown_fields where security-relevant, schema registered, all consumers (incl. JS) updated (R3.*) — no wire types; one new internal runtime message
- [x] Config absence = most restrictive; fail-closed if enforcement can't start (R5.*) — this is the change: absent inbox now fails closed and is reported, instead of silently defaulting
- [x] Logs/status claim only what was verified; background-job failures are surfaced (R6.*) — Setup and the self-test now name the relay rather than asserting it
- [x] "Process dies on the next line" answered for every mutation touched (R2.1) — the setting is a single idempotent write; a death before it re-runs the backfill next boot
- [x] Deviations from this guide flagged explicitly with rule numbers — none

…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>
@stormer78
stormer78 merged commit 97b38e9 into main Aug 31, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/inbox-mediator-from-agent branch August 31, 2026 06:08
stormer78 added a commit that referenced this pull request Aug 31, 2026
… 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>
stormer78 added a commit that referenced this pull request Aug 31, 2026
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>
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Aug 31, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 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

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/inbox-mediator-from-agentmain
Validated 2026-09-05
Scan ID 3409f5f7
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 9 · findings: 3

Module Files scanned Findings
packages/extension 9 3

Executive Summary

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

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


🔒 Security Issues

Confirmed Vulnerabilities (2)

🟡 Inbox mediator auto-adopted from unauthenticated agent-advertised data without integrity/authorization check

Field Detail
Severity MEDIUM
Location packages/extension/src/active-vta.ts:66
Finding ID github_pr-0b3889aa633c
CWE CWE-862
OWASP A01:2021 - Broken Access Control
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE-862 missing authorization is confirmed by code evidence (readAgentMediatorDid trusts stored data with no signature/integrity check), and the scenario has real security consequences (interception of DIDComm consent/approval messages). However, exploitation requires a MITM during a narrow onboarding window or a compromised mediator agent — not a direct remote attack against an always-exposed endpoint. No CVE/EPSS/exploit maturity data exists (first-party code), reachability is unconfirmed by the scanner ('no-info'), and scanner confidence is only 60%. This keeps it at medium: real risk, but conditional preconditions and unconfirmed exploitation in the wild prevent escalation to high/critical.
  • Composite score: 5.1
  • Environment: unknown

📝 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: packages/extension/src/active-vta.ts:66

export async function readAgentMediatorDid(): Promise<string | undefined> {
  const stored = await chrome.storage.local.get("pnm-connection/v3");
  return parseAgentMediatorDid(stored["pnm-connection/v3"]);
}

🎯 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.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 60%
  • AI Validation Evidence: EVIDENCE FOUND: active-vta.ts defines readAgentMediatorDid()/parseAgentMediatorDid() (per description) which reads chrome.storage.local key 'pnm-connection/v3' with no signature/HMAC or authorization check — comparable functions in the same file, e.g. parseActiveVtaDid: 'if (typeof raw !== "string") return null; try { const parsed = JSON.parse(raw) as {...}; return parsed.state?.connections?.activeVtaDid ?? null; } catch { return null; }' show the same pattern of trusting the raw chrome.storag
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 getWalletMediatorDid() return type changed to allow undefined; downstream comparisons may mask null dereference risk

Field Detail
Severity MEDIUM
Location packages/extension/src/holder.ts:29
Finding ID github_pr-b691233d6e6e
CWE CWE-476
OWASP A04:2021-Insecure Design
Detection Source threat_model

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — CWE-476 null dereference concerns are only theoretical here: the scanner explicitly notes the downstream usage (strict equality comparison and an explicit unset check) does not throw on undefined. No confirmed crash path, no attacker-controlled input, and no production incident evidence. Low severity is appropriate and consistent with the original rating.
  • Composite score: 5.7
  • Environment: production

📝 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: packages/extension/src/holder.ts:29

export async function getWalletMediatorDid(): Promise<string | undefined> {
  return (await getSettings()).mediatorDid;
}

🎯 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).

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 30%
  • AI Validation Evidence: EVIDENCE FOUND: holder.ts contains 'export async function getWalletMediatorDid(): Promise<string | undefined> { return (await getSettings()).mediatorDid; }' — wait, actual quoted source_files holder.ts does not show this exact function name but shows buildHolderSecretWrap and loadHolder; however the finding's evidence snippet directly quotes holder.ts line 29 with this exact signature, and getSettings() is imported and used in holder.ts confirming settings.mediatorDid is optional. EVIDENCE NOT F
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

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

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

🔵 Unsafe Formatstring (3 occurrences)

Field Detail
Severity LOW
Location packages/extension/src/offscreen.ts: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.

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

🛡️ Threat Model & Affect Analysis — PR #148

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/inbox-mediator-from-agentmain
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:

  1. 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).
  2. Attacker writes a crafted pnm-connection/v3 JSON blob into chrome.storage.local containing state.connections.vtas[<did>].mediatorDid = 'did:webvh:attacker-controlled-mediator' and sets activeVtaDid to that DID.
  3. On next extension boot, startInboundListener() in background.ts calls readAgentMediatorDid() (active-vta.ts) which reads and JSON.parses the tampered pnm-connection/v3 value via parseAgentMediatorDid().
  4. parseAgentMediatorDid() returns the attacker's mediator DID because conns?.activeVtaDid matches the injected VTA entry (lines: const active = conns?.activeVtaDid ? vtas[conns.activeVtaDid]?.mediatorDid : undefined;).
  5. inboxToAdopt((await getSettings()).mediatorDid, attackerMediatorDid) in config.ts returns the attacker value because current (mediatorDid) is unset (if (current) return undefined; return advertised;).
  6. setSettings({ mediatorDid: adopt }) persists the attacker-controlled mediator as the wallet's permanent inbox, and all future RP/executor confirm and task-consent/request DIDComm 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:

  1. 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_connectable access) sends { type: RUNTIME_RESTART_INBOX }.
  2. background.ts's chrome.runtime.onMessage.addListener matches on (message as { type?: string })?.type === RUNTIME_RESTART_INBOX with no sender.id or origin verification visible in the shown code.
  3. startInboundListener() is invoked repeatedly, tearing down and re-establishing all DIDComm mediator sessions for every known VTA DID.
  4. 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.
  5. 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:

  1. 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 } : {})).
  2. If the agent never advertised a mediator and no prior connection recorded one, readAgentMediatorDid() returns undefined and inboxToAdopt also returns undefined, leaving mediatorDid permanently unset.
  3. getWalletMediatorDid() in holder.ts now legitimately returns undefined instead of throwing or falling back.
  4. Any RP-initiated confirm request or task-consent/request DIDComm message sent to this wallet's (nonexistent) inbox silently never arrives, with no proactive alert to the user beyond the passive runDiagnostics self-test (which the user must manually trigger).
  5. 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:

  1. background.ts's startInboundListener() runs on every extension boot (browser restart, extension reload, service-worker wake).
  2. It unconditionally calls inboxToAdopt((await getSettings()).mediatorDid, await readAgentMediatorDid()) and, if a value is returned, calls setSettings({ mediatorDid: adopt }) with only a console.info log — no user-facing confirmation, no write to any tamper-evident audit trail.
  3. Because the mediatorDid comes from the potentially attacker-influenced pnm-connection/v3 store (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.
  4. 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.
  5. 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:

  1. getWalletMediatorDid() in holder.ts changes its declared return type from Promise<string> to Promise<string | undefined>.
  2. 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 pass undefined into a security-relevant comparison (e.g. s.mediatorDid === inbox in transportHealthSnapshot).
  3. Provided diff shows offscreen.ts's transportHealthSnapshot() and runDiagnostics() were updated correctly, but the recon notes explicitly flag that a full call-site audit was not possible with the reduced file set.
  4. An unreviewed or third-party-contributed call site that silently treats undefined as falsy-but-valid could mark a session isInbox: false when it should be true (or vice versa), causing the self-test or health snapshot to misreport wallet reachability.
  5. 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:

  1. During onboarding (doOnboardConnect in offscreen.ts), the wallet contacts an agent and receives services.didcomm?.mediatorDid as the agent's advertised mediator.
  2. 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.
  3. inboxToAdopt((await getSettings()).mediatorDid, services.didcomm?.mediatorDid) returns the attacker's DID because the current setting is unset (fresh onboarding).
  4. 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.
  5. 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:

  1. Extension boot triggers startInboundListener() automatically, beginning a getSettings()readAgentMediatorDid()setSettings() sequence.
  2. Before this sequence completes (all async), the newly added RUNTIME_RESTART_INBOX message handler (STRIDE-2) is triggered — either legitimately (user changed the mediator by hand in Setup) or maliciously — invoking startInboundListener() a second time concurrently.
  3. Both invocations independently call getSettings(), observe mediatorDid as unset (before the first write completes), and both compute inboxToAdopt results based on stale reads.
  4. 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.
  5. 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:

  1. Attacker with storage write access (see STRIDE-1 precondition) sets pnm-connection/v3 to a JSON string whose state.connections.vtas value is an object with a crafted __proto__ or extremely large key set.
  2. 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.
  3. 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 inside startInboundListener().
  4. 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.
  5. 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:

  1. An automatic mediator adoption occurs either at boot (startInboundListener) or during onboarding (doOnboardConnect), both logging only via console.info("[pnm inbound] inbox mediator backfilled from agent:", adopt) or console.info("[pnm onboard] inbox mediator set from agent:", inbox).
  2. 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.
  3. 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).
  4. 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.
  5. 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:

  1. The diff removes the module-level _walletMediatorDid cache and reads getSettings() fresh on every call to walletMediatorDid().
  2. getSettings() performs an IndexedDB/chrome.storage read on every invocation rather than returning a cached, already-resolved string.
  3. 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).
  4. 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.
  5. 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:

  1. A local malicious actor (compromised sibling extension, malicious page abusing externally_connectable, or automated test harness left enabled in production) repeatedly sends RUNTIME_RESTART_INBOX messages in a tight loop.
  2. Each message triggers a full startInboundListener() cycle: closing existing mediator WebSocket/DIDComm sessions and re-establishing new ones for every known VTA DID.
  3. 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.
  4. 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.
  5. 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
Loading

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
Loading

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
Loading

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants