Skip to content

feat(consent): surface pending approvals from durable state, not a window - #110

Merged
stormer78 merged 1 commit into
mainfrom
feat/pending-approval-badge
Aug 7, 2026
Merged

feat(consent): surface pending approvals from durable state, not a window#110
stormer78 merged 1 commit into
mainfrom
feat/pending-approval-badge

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

A prompt window that must appear at an arbitrary moment is the least reliable thing this extension can attempt. It requires a live service worker, a live offscreen document, and a promise chain spanning both, on a runtime free to kill either at any moment.

Every failure in this series was a different edge of that single design problem:

PR Edge
#106 chrome.windows.create failing returned without settling → hung forever
#108 handleInbound had no catch; a throw vanished as an unhandled rejection
#109 sendMessage from an offscreen document cannot reliably start a terminated worker

Each was real. None was sufficient, because the shape underneath them stayed the same.

What this changes

A badge has none of those dependencies. It is derived from the durable record the inbound path already writes before it acks the mediator (pending.ts), so it is correct after any teardown — and it gives the user a way in, rather than depending on a window finding its way out.

refreshPendingBadge() counts approver-bound pending records and sets the action badge. It runs on:

  • service-worker startup
  • a consent port opening (a request was just durably recorded)
  • that port disconnecting (the interaction ended)

Every wake is a chance to be correct. It's idempotent and cheap, so no single missed call can leave the badge lying, and failures are swallowed with a warn — a cosmetic surface must never break a wake path.

Why read from pending.ts

That record is written before the mediator is acked, which is precisely the point at which the extension has taken responsibility for the request. Deriving the badge from it means the badge cannot claim a request the recipient never durably held, nor miss one it did.

pending.ts already had the right semantics — write-before-ack, listPendingInbound, drain-on-boot. It was wired as crash recovery rather than as the spine of the flow. This is the first step of inverting that.

Scope

The window remains the fast path; this is the floor beneath it. Deliberately not in this PR, and each worth its own review:

  • Notification on arrival — a notification click is a user gesture, which is the reliable way to open a window
  • Split authorize from deliver — sign the decision in the popup while the biometric-unlocked key is available, then transmit separately; today one chain needs the key and the socket and two live contexts at once
  • Drain on every wake — including handlePushWake, which currently calls startInboundListener() and never surfaces pending approvals, despite the VTA already ringing that doorbell via trigger_gateway_wake

Lint clean, 223 tests pass, builds.

…ndow

A prompt window that must appear at an arbitrary moment is the least
reliable thing this extension can attempt. It needs a live service worker,
a live offscreen document, and a promise chain spanning both, on a runtime
free to kill either at any time. Every failure chased in this series --
a create() that hung (#106), a swallowed throw (#108), a send that could
not start a terminated worker (#109) -- was a different edge of that one
design problem.

A badge has none of those dependencies. It is derived from the durable
record the inbound path already writes before it acks the mediator
(pending.ts), so it is correct after any teardown, and it gives the user a
way IN rather than depending on a window finding its way OUT.

refreshPendingBadge() counts approver-bound pending records and sets the
action badge. It runs on service-worker startup and when a consent port
opens or closes -- every wake is a chance to be correct. Idempotent and
cheap, so no single missed call can leave it lying, and a failure is
swallowed with a warn: a cosmetic surface must never break a wake path.

The window remains the fast path. This is the floor beneath it: the first
thing in this flow that does not depend on two ephemeral contexts being
alive simultaneously.

First step of the larger inversion -- making the durable record the spine
of the flow rather than crash recovery for it.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit c4b08a8 into main Aug 7, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/pending-approval-badge branch August 7, 2026 14:35
stormer78 added a commit that referenced this pull request Aug 7, 2026
)

parseTaskConsentRequest returned "not-a-task-consent-request" for two
completely different situations: a message not addressed to this handler,
and a genuine consent request whose payload is unusable.

dispatchInbound keys on that reason to decide whether to stay quiet:

    if (consent.reason !== "not-a-task-consent-request") { warn; return; }
    // Anything else is ignored.   <- silent

So a malformed consent request was discarded in total silence -- no
prompt, no log -- and handleInbound's finally then cleared its pending
record. The result is indistinguishable from a message that never
arrived, which is how it presented: arrival logged by #105, then nothing
at all, and a pending-approval badge (#110) that counted zero because the
record was already gone.

Malformed payloads now return "malformed-payload", so the existing warn
path reports them with their detail. The silent reason keeps its single
honest meaning: not addressed to this handler.

The test that pinned the old shared reason now pins the distinction and
the detail, with the reasoning recorded -- it was asserting the exact
behaviour that hid this.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Review

⚠️ Security Report — 3 confirmed issues

PR #110vta-browser-plugin • Review the attached reports for details and recommended actions.


🤖 AI-Generated — This review validates findings against source code.
Remediation suggestions should be tested before applying. Engineers own the final implementation.
When in doubt, consult the Security team.


🎯 Scope: changes only. This review covers only the code introduced by this MR/PR's diff, so a clean result means "no new issues" — not "no issues at all." Whole-codebase coverage is handled by the scheduled repository scans.

📊 Summary

Severity Issues
🔵 Low 3
Total Confirmed 3
⚠️ Must-Review-By-Human 3

⚠️ 3 finding(s) need human review — the automated validation was inconclusive (insufficient evidence). These are not dismissed; please have a developer / the Security team read and decide.


⚠️ Must-Review-By-Human (3) — click to collapse
  • 🟡 Unauthenticated trust in isApprover flag enabling local badge/UI spoofing — EVIDENCE FOUND: background.ts refreshPendingBadge reads const pending = await listPendingInbound(new IndexedDBKVStore()); const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length; and uses this to set badge text…
  • 🔵 Unsafe Formatstring in background.ts:1655 — EVIDENCE FOUND: The finding references line 1655 of background.ts with an empty code_snippet and vague description about 'string concatenation with a non-literal variable in a util.format / console.log function'.
  • Unpinned internal dependency import (@openvtc/pnm-core) introduces supply-chain risk to privileged background context — EVIDENCE FOUND: import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core"; is present at background.ts line 11, and this import is used in the privileged background service worker context alongside DIDComm/token handling (p…

These were validated up to a point but need a human to make the final call.


📎 Reports

🔒 Security Validation Report (mandatory review — confirmed, materialised security issues)

📄 Open full Security Validation Report — validation_report_PR110_2026-08-07T15-11-02.md

🛡️ Security Validation Report — PR #110

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/pending-approval-badgemain
Validated 2026-08-07
Scan ID 98cb95a7
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 1 · findings: 6

Module Files scanned Findings
packages/extension 1 6

Executive Summary

Category Confirmed Must-Review-By-Human False Positive Duplicate Not Applicable Total
Security Issues 3 3 0 0 0 6

⚠️ 3 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 (3)

🔵 Unbounded IndexedDB scan on every onConnect/onDisconnect event (resource exhaustion / local DoS)

Field Detail
Severity LOW
Location packages/extension/src/background.ts:478
Finding ID github_pr-f753439f9794
CWE CWE-400, CWE-770
OWASP A04:2021 - Insecure Design
MITRE ATT&CK T1499 - Endpoint Denial of Service
CAPEC CAPEC-125
DREAD 3.6
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: The refreshPendingBadge() function is invoked unconditionally on every port connect and disconnect event without any rate limiting, allowing repeated expensive IndexedDB scans to be triggered in quick succession.

📝 Description:

Repeated flooding can keep the service worker busy processing badge refreshes, potentially delaying legitimate DIDComm/consent message handling and draining device battery/CPU during sustained abuse.

🧪 Proof of Concept:

Every connect and disconnect event unconditionally triggers a full async IndexedDB scan (refreshPendingBadge -> listPendingInbound) with no debounce, so an attacker who can rapidly open/close the port can trigger unbounded repeated scans.

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== CONSENT_KEEPALIVE_PORT) return;
  // A consent port opening means a request was just durably recorded; a
  // disconnect means the interaction ended. Both change the count.
  void refreshPendingBadge();
  port.onDisconnect.addListener(() => {
    void refreshPendingBadge();
    // Nothing to clean up — the port exists only to hold the worker awake.
  });
});

Vulnerable lines: 478, 490

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

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== CONSENT_KEEPALIVE_PORT) return;
  void refreshPendingBadge();
  port.onDisconnect.addListener(() => {
    void refreshPendingBadge();
  });
});

💥 Impact:

Repeated flooding can keep the service worker busy processing badge refreshes, potentially delaying legitimate DIDComm/consent message handling and draining device battery/CPU during sustained abuse.

Confidentiality: none · Integrity: none · Availability: low — repeated IndexedDB scans and badge writes could delay processing of legitimate consent/DIDComm flows and drain battery/CPU on the victim device

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001 (chrome.runtime.onConnect, CONSENT_KEEPALIVE_PORT) → refreshPendingBadge() → IndexedDBKVStore.listPendingInbound() at background.ts:462

⚖️ Triage Factors:

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

Attack scenario: A page or script able to open a port named CONSENT_KEEPALIVE_PORT against this extension can flood connect/disconnect cycles to repeatedly trigger full IndexedDB scans, degrading responsiveness.

🔧 Remediation:

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

Coalesces multiple rapid refresh triggers into a single debounced call within a 300ms window, preventing unbounded repeated IndexedDB scans while preserving correctness (last write wins on durable state).

Vulnerable code:

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== CONSENT_KEEPALIVE_PORT) return;
  void refreshPendingBadge();
  port.onDisconnect.addListener(() => {
    void refreshPendingBadge();
  });
});

Secure code:

let refreshTimer: ReturnType<typeof setTimeout> | null = null;
function scheduleRefreshPendingBadge(): void {
  if (refreshTimer) return;
  refreshTimer = setTimeout(() => {
    refreshTimer = null;
    void refreshPendingBadge();
  }, 300);
}

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== CONSENT_KEEPALIVE_PORT) return;
  scheduleRefreshPendingBadge();
  port.onDisconnect.addListener(() => {
    scheduleRefreshPendingBadge();
  });
});

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 75%
  • AI Validation Evidence: EVIDENCE FOUND: chrome.runtime.onConnect.addListener((port) => { if (port.name !== CONSENT_KEEPALIVE_PORT) return; void refreshPendingBadge(); port.onDisconnect.addListener(() => { void refreshPendingBadge(); }); }); triggers a full refreshPendingBadge() call (which performs an unthrottled listPendingInbound() IndexedDB scan) on both connect and disconnect with no debounce, throttle, or dedup guard visible anywhere in the provided background.ts excerpt. EVIDENCE NOT FOUND: No debounce/throttle/in-flight lock implementation was found in the provided code. CHANGED VS PRE-EXISTING: CHANGED — this onConnect listener and the refreshPendingBadge call chain are part of this MR (introduced_at 2026-08-07T14:35:23, background.ts is the changed file). VERDICT JUSTIFICATION: The reachable code path (connect/disconnect → refreshPendingBadge → listPendingInbound) is directly quoted and confirmed unthrottled; severity is appropriately low/local since it requires local port access, but the resource-exhaustion pattern is real and unmitigated in the given code.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🔵 Silent, unrecoverable badge desynchronization on IndexedDB failure (missing failure escalation)

Field Detail
Severity LOW
Location packages/extension/src/background.ts:467
Finding ID github_pr-c01233483053
CWE CWE-755, CWE-390
OWASP A09:2021 - Security Logging and Monitoring Failures
CAPEC CAPEC-227
DREAD 1.8
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

Summary: The error handler for refreshPendingBadge only logs to the console (invisible to normal users) and takes no corrective action, so any IndexedDB read failure leaves the pending-approval badge silently stuck at a stale value.

📝 Description:

A user could be left with a badge showing zero or an outdated count of pending DID approval requests, causing them to overlook a legitimate, potentially time-sensitive consent request.

🧪 Proof of Concept:

The catch block has no fallback behavior other than logging; there is no user-visible signal that the badge state is unreliable, and no retry mechanism to self-heal from transient failures.

async function refreshPendingBadge(): Promise<void> {
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;
    await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });
    if (waiting > 0) {
      await chrome.action.setBadgeBackgroundColor({ color: "#8B1A1A" });
    }
  } catch (err) {
    // Never let a cosmetic surface break a wake path.
    console.warn("[pnm consent] could not refresh the pending badge:", err);
  }
}

Vulnerable lines: 460, 470

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

} catch (err) {
  console.warn("[pnm consent] could not refresh the pending badge:", err);
}

💥 Impact:

A user could be left with a badge showing zero or an outdated count of pending DID approval requests, causing them to overlook a legitimate, potentially time-sensitive consent request.

Confidentiality: none · Integrity: low — stale/incorrect approval-pending indicator persists silently · Availability: low — user-visible feedback about pending DID approvals can become permanently wrong until an unrelated successful refresh occurs

🧭 Reachability:

  • Network exposure: none
  • Auth barrier: none
  • Attack path: EP-002 (module load/wake) → refreshPendingBadge() → IndexedDBKVStore/listPendingInbound throw at background.ts:462 → swallowed at line 467-469

⚖️ Triage Factors:

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

Attack scenario: If IndexedDB throws (quota exhaustion, corruption, or an update race), the badge silently freezes at its last value with no visible error, potentially misleading the user about pending DID approvals indefinitely.

🔧 Remediation:

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

On failure, the badge is switched to a distinct visible error indicator instead of silently freezing at a possibly-wrong prior value, giving the user a signal that the pending-approval count is unreliable.

Vulnerable code:

} catch (err) {
  console.warn("[pnm consent] could not refresh the pending badge:", err);
}

Secure code:

} catch (err) {
  console.warn("[pnm consent] could not refresh the pending badge:", err);
  try {
    await chrome.action.setBadgeText({ text: "!" });
    await chrome.action.setBadgeBackgroundColor({ color: "#555555" });
  } catch {
    // best-effort; nothing further to do if even the fallback badge write fails
  }
}

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 75%
  • AI Validation Evidence: EVIDENCE FOUND: The catch block is exactly } catch (err) { console.warn("[pnm consent] could not refresh the pending badge:", err); } — no retry, no user-facing error state, no escalation logic present. EVIDENCE NOT FOUND: No alternate error-handling path, retry mechanism, or error-state badge/icon was found in the provided source. CHANGED VS PRE-EXISTING: CHANGED — this catch block belongs to refreshPendingBadge, a function newly introduced by this MR (introduced_at matches the MR's other changes to background.ts). VERDICT JUSTIFICATION: The quoted code directly confirms the silent-failure behavior with no escalation, matching the finding precisely.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🔵 Unsynchronized concurrent badge refresh causing stale pending-approval count (race condition)

Field Detail
Severity LOW
Location packages/extension/src/background.ts:460
Finding ID github_pr-69120885ab63
CWE CWE-362, CWE-367
OWASP A04:2021 - Insecure Design
MITRE ATT&CK T1499
CAPEC CAPEC-26
DREAD 2.2
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

Summary: refreshPendingBadge() performs an async read-then-write with no mutual exclusion; concurrent invocations from module load, onConnect, and onDisconnect can interleave and let an older result overwrite a newer one.

📝 Description:

The pending-approval badge count can display stale/incorrect values, potentially causing a user to overlook a legitimate pending DID approval request or believe one is still outstanding after it was handled.

🧪 Proof of Concept:

The function has no sequencing token; if two invocations run concurrently, whichever's await chain resolves last wins the final chrome.action.setBadgeText call, regardless of which read was more current.

async function refreshPendingBadge(): Promise<void> {
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;
    await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });
    if (waiting > 0) {
      await chrome.action.setBadgeBackgroundColor({ color: "#8B1A1A" });
    }
  } catch (err) {
    console.warn("[pnm consent] could not refresh the pending badge:", err);
  }
}

Vulnerable lines: 460, 473

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

async function refreshPendingBadge(): Promise<void> {
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;
    await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });

💥 Impact:

The pending-approval badge count can display stale/incorrect values, potentially causing a user to overlook a legitimate pending DID approval request or believe one is still outstanding after it was handled.

Confidentiality: none · Integrity: low — the badge count displayed to the user can be transiently incorrect, potentially masking a real pending DID approval · Availability: none

🧭 Reachability:

  • Network exposure: none
  • Auth barrier: none
  • Attack path: EP-002 (module load) + EP-001 (onConnect/onDisconnect) → concurrent refreshPendingBadge() invocations → out-of-order chrome.action.setBadgeText writes at background.ts:464

⚖️ Triage Factors:

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

Attack scenario: Two overlapping refreshPendingBadge calls triggered by near-simultaneous wake events can resolve out of order, leaving the badge showing a stale count.

🔧 Remediation:

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

A monotonic generation counter ensures that only the result of the most recently initiated refresh is ever written to the badge, discarding stale results from superseded calls.

Vulnerable code:

async function refreshPendingBadge(): Promise<void> {
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    ...
  } catch (err) { ... }
}

Secure code:

let latestGeneration = 0;
async function refreshPendingBadge(): Promise<void> {
  const generation = ++latestGeneration;
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    if (generation !== latestGeneration) return; // superseded by a newer call
    const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;
    await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });
    if (waiting > 0) {
      await chrome.action.setBadgeBackgroundColor({ color: "#8B1A1A" });
    }
  } catch (err) {
    console.warn("[pnm consent] could not refresh the pending badge:", err);
  }
}

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 70%
  • AI Validation Evidence: EVIDENCE FOUND: refreshPendingBadge() has no locking/sequencing: async function refreshPendingBadge(): Promise<void> { try { const pending = await listPendingInbound(new IndexedDBKVStore()); const waiting = pending.filter(...).length; await chrome.action.setBadgeText(...); ... It is called from module load, onConnect, and onDisconnect with no mutex, generation counter, or promise-chaining guard. EVIDENCE NOT FOUND: No locking mechanism (e.g., a pending-promise variable or sequence number) was found anywhere in the provided background.ts excerpts. CHANGED VS PRE-EXISTING: CHANGED — refreshPendingBadge is new code introduced by this MR per introduced_at timestamp and is called from newly-added onConnect/onDisconnect handlers also added in this MR. VERDICT JUSTIFICATION: The race condition is real given multiple unsynchronized invocation triggers confirmed in code, though impact is limited to a cosmetic badge value (low severity, as scored).
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

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

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.

🟡 Unauthenticated trust in isApprover flag enabling local badge/UI spoofing

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:462
Finding ID github_pr-71080ead0e2b
CWE CWE-345, CWE-807
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1565 - Data Manipulation
CAPEC CAPEC-176, CAPEC-141
DREAD 3.4
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: The badge-refresh logic filters pending-inbound records by an isApprover boolean read directly from local IndexedDB storage without any cryptographic integrity check, so a party with local write access to that storage (a separate compromise, not this diff's own vulnerability) can spoof the badge's pending-approval count.

📝 Description:

An attacker who already has local storage write access can make the extension display a false pending-approval alert, potentially manipulating user behavior (opening the extension, rushing an approval decision) as a precursor to a social-engineering or consent-manipulation attack against the DID/DIDComm approval flow.

🧪 Proof of Concept:

The filter on p.isApprover trusts the stored value with no verification that it corresponds to an authentic, unmodified DIDComm approval request; the value's provenance is not re-validated at read time in this function.

async function refreshPendingBadge(): Promise<void> {
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;
    await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });
    if (waiting > 0) {
      await chrome.action.setBadgeBackgroundColor({ color: "#8B1A1A" });
    }
  } catch (err) {
    console.warn("[pnm consent] could not refresh the pending badge:", err);
  }
}

Vulnerable lines: 460, 473

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

const pending = await listPendingInbound(new IndexedDBKVStore());
const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;

💥 Impact:

An attacker who already has local storage write access can make the extension display a false pending-approval alert, potentially manipulating user behavior (opening the extension, rushing an approval decision) as a precursor to a social-engineering or consent-manipulation attack against the DID/DIDComm approval flow.

Confidentiality: none · Integrity: medium — displayed approval count can be falsified, and if the downstream approval UI shares the same trust assumption (not visible in this diff), this could contribute to social-engineering a user into approving a forged DID/DIDComm request · Availability: none

🧭 Reachability:

  • Network exposure: none
  • Auth barrier: none
  • Attack path: Local storage write (separate compromise, e.g. malicious co-installed extension or compromised dependency) → IndexedDB pending-inbound store → refreshPendingBadge() reads unvalidated isApprover at background.ts:463 → chrome.action.setBadgeText/setBadgeBackgroundColor (line 464-467)

⚖️ Triage Factors:

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

Attack scenario: An attacker with local write access to this extension's IndexedDB (via a separate compromise) can insert fake isApprover:true records, causing the badge to display a fabricated, alarming pending-approval count that could pressure the user into opening the extension.

🔧 Remediation:

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

Adds an integrity-verification step so that only records whose provenance can be cryptographically confirmed contribute to the displayed count, preventing an attacker with mere storage write access from spoofing the badge.

Vulnerable code:

const pending = await listPendingInbound(new IndexedDBKVStore());
const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;

Secure code:

const pending = await listPendingInbound(new IndexedDBKVStore());
const waiting = pending.filter((p) => p.isApprover && verifyPendingRecordIntegrity(p)).length;
// verifyPendingRecordIntegrity should check a MAC/signature written by pending.ts
// at record-creation time, tied to the verified DIDComm sender, before trusting isApprover.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: background.ts refreshPendingBadge reads const pending = await listPendingInbound(new IndexedDBKVStore()); const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length; and uses this to set badge text/color with no signature/MAC check visible in this function. EVIDENCE NOT FOUND: The write path (pending.ts) that populates the IndexedDB store is not in source_files, so I cannot confirm whether provenance/signature checks occur upstream before a record is durably written, nor can I confirm whether the actual consent-approval flow (separate from the badge) re-validates the request cryptographically before granting authority. The finding's own description acknowledges 'the same durable record the inbound path writes before it acks the mediator', implying some upstream write discipline not shown here. CHANGED VS PRE-EXISTING: CHANGED — background.ts is in this MR's diff scope and refreshPendingBadge/isApprover filter logic (lines 460-463) are new code per the introduced_at metadata. VERDICT JUSTIFICATION: The badge is cosmetic (display-only) per the code and description; without visibility into pending.ts's write-time validation or the real approval-flow's authorization checks, I cannot confirm this is actually exploitable to affect a security decision beyond a misleading UI number, but I also cannot dismiss it as safe. This is inconclusive given missing upstream file evidence.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🔵 Unsafe Formatstring in background.ts:1655

Field Detail
Severity LOW
Location /tmp/asec-scan-b2e06ad66f114322e71699c3343b07e3/cloned-repo/packages/extension/src/background.ts:1655
Finding ID github_pr-b89f23ef02a9
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

📝 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: 20%
  • AI Validation Evidence: EVIDENCE FOUND: The finding references line 1655 of background.ts with an empty code_snippet and vague description about 'string concatenation with a non-literal variable in a util.format / console.log function'. No actual code content was provided for this line in source_files (the provided background.ts excerpts do not extend to line 1655, and no console.log/format-string call is visible anywhere in the given content). EVIDENCE NOT FOUND: No code snippet, no surrounding function, no confirmation that this line even exists in the changed diff; the evidence_type is 'code' but content is blank. CHANGED VS PRE-EXISTING: Cannot determine — background.ts is a changed file in this MR, but without the actual line content at 1655 I cannot confirm whether this specific code is part of the diff or pre-existing. VERDICT JUSTIFICATION: Insufficient evidence to confirm or dismiss; the empty code_snippet and lack of file content at this line prevent verification of an actual vulnerable sink.
  • 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.

⚪ Unpinned internal dependency import (@openvtc/pnm-core) introduces supply-chain risk to privileged background context

Field Detail
Severity INFORMATIONAL
Location packages/extension/src/background.ts:11
Finding ID github_pr-5af64b8984fe
CWE CWE-1104, CWE-829
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1195 - Supply Chain Compromise
CAPEC CAPEC-538
DREAD 2.6
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

Summary: The diff introduces a new dependency on @openvtc/pnm-core into the background service worker, which the file's own comments describe as handling sensitive DIDComm/REST token flows; without visibility into lockfile pinning or CI integrity checks, this represents a supply-chain hardening gap rather than a confirmed vulnerability.

📝 Description:

No current demonstrated impact. If realized in the future (upstream compromise of @openvtc/pnm-core), the impact would be full compromise of DIDComm keys and REST tokens managed by this background worker.

🧪 Proof of Concept:

The header comments confirm this file's execution context handles REST tokens and DIDComm flows; the newly added import runs unconditionally in that same context, meaning any future compromise of the imported package would have access to that sensitive material — this is a structural observation, not proof of an existing compromise.

// REST flow: content → RUNTIME_LOGIN → consent → offscreen REST login → tokens.
// DIDComm flow: content → RUNTIME_LOGIN_DIDCOMM → consent → offscreen doc.

import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core";
import {
  parseActiveVtaDid,
  parseAllVtaDids,

Vulnerable lines: 1, 15

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

import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core";

💥 Impact:

No current demonstrated impact. If realized in the future (upstream compromise of @openvtc/pnm-core), the impact would be full compromise of DIDComm keys and REST tokens managed by this background worker.

Confidentiality: high (speculative) — if the upstream package were compromised, it would run with full access to the background worker's DIDComm keys and REST tokens per file header comments · Integrity: high (speculative) · Availability: high (speculative)

🧭 Reachability:

  • Network exposure: none
  • Auth barrier: none
  • Attack path: Build-time dependency resolution → @openvtc/pnm-core package code → bundled into background.ts execution context (privileged, holds tokens/keys per file header comments)

⚖️ Triage Factors:

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

Attack scenario: If @openvtc/pnm-core were compromised upstream (not evidenced in this scan), the newly added import would grant that code execution inside the extension's most privileged context.

🔧 Remediation:

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

Pinning exact versions with an enforced, committed lockfile and verifying package integrity in CI reduces the risk of an unnoticed malicious update reaching the privileged background context.

Vulnerable code:

import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core";

Secure code:

// package.json: pin exact version and enforce integrity hash
// "@openvtc/pnm-core": "1.2.3" with lockfile (package-lock.json/pnpm-lock.yaml) committed and verified in CI
// CI step example (conceptual):
// npm ci --ignore-scripts && npm audit signatures

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 40%
  • AI Validation Evidence: EVIDENCE FOUND: import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core"; is present at background.ts line 11, and this import is used in the privileged background service worker context alongside DIDComm/token handling (per file's own header comments referenced in other findings). EVIDENCE NOT FOUND: No lockfile (package-lock.json/pnpm-lock.yaml), no CI configuration, and no integrity-hash pinning evidence was provided in source_files to confirm or deny supply-chain protections for @openvtc/pnm-core. CHANGED VS PRE-EXISTING: CHANGED — this import line is explicitly part of this MR (introduced_at 2026-08-07T14:35:23) adding new usage of pnm-core in background.ts. VERDICT JUSTIFICATION: This is an informational/design-risk finding about absence of supply-chain controls; since no lockfile/CI evidence was available to confirm or refute integrity pinning, this cannot be validated as an active exploit but also cannot be dismissed — appropriately kept for human review at low confidence given its informational nature.
  • 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.

🛡️ Threat Model & Affect Analysis (supplementary — theoretical threats and MR impact analysis)

🛡️ Open full Threat Model & Affect Analysis — threat-modelling_affect-analysis_report_PR110_2026-08-07T17-56-55.md

🛡️ Threat Model & Affect Analysis — PR #110

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/pending-approval-badgemain
Generated 2026-08-07

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Validation 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

Adds a durable-state-derived toolbar badge that surfaces the count of pending inbound approval requests, refreshed on service worker wake and consent keepalive port connect/disconnect, as a more reliable complement to the existing popup-window approval flow.

Diff: +39 / -0 lines
Types: feature, security, refactor

Risk Assessment

  • Overall Risk: low
  • Review Priority: medium
  • Pentest Needed: false
  • Security Review Needed: false
  • Breaking Changes: false

This is a small (39-line, single-file), purely additive, read-only feature that introduces no new network egress, no new writes, no new privileged APIs, and no changes to authentication/authorization logic. The chrome.action badge APIs used are low-privilege and cosmetic. The residual concerns are: (1) the badge's correctness and trustworthiness depend entirely on an unauthenticated upstream write path (pending.ts) that is out of scope for this diff and cannot be verified here — this is the most substantive concern and warrants confirmation rather than a dedicated security review of this diff itself; (2) minor hygiene issues (raw error logging, no debounce) that are low-effort to fix; (3) a pre-existing weak port-name-only gate that is now exercised slightly more, which predates this PR and is not introduced by it. None of these findings individually or collectively rise to a level requiring a formal security review or penetration test — the changes are additive, fail-safe (errors are caught and swallowed without breaking the wake path), and confined to a cosmetic UI surface. A manual code review is still recommended given the security-critical file classification and the trust-boundary/data-provenance questions raised, but this should be a standard PR review, not an escalated security review.

Review Focus Areas:

  • Correctness dependency on isApprover field shape from @openvtc/pnm-core
  • Error logging hygiene in the catch block
  • Absence of debounce on port-triggered refreshes
  • Pre-existing port-name-only gate now exercised more frequently

Pentest Focus:

  • Verify manifest.json externally_connectable/permissions do not expose CONSENT_KEEPALIVE_PORT or badge triggers to untrusted web origins
  • Validate that pending.ts authenticates/verifies mediator-originated messages before recording pending-approval entries surfaced by this badge
  • Confirm the actual consent-approval decision path independently re-validates requests rather than trusting listPendingInbound output
  • Stress-test rapid port connect/disconnect cycling for resource use given the lack of debouncing

⚠️ Security Implications

🔵 Badge count derived from unauthenticated local IndexedDB data without integrity verification

Badge count derived from unauthenticated local IndexedDB data without integrity verification

Action: Ensure pending.ts validates message provenance/signatures before durably recording entries, and confirm the actual consent-approval flow independently re-validates requests rather than trusting listPendingInbound output as an authorization source.

⚪ Unsanitized error object logged to console on badge refresh failure

Unsanitized error object logged to console on badge refresh failure

Action: Log only err.message (and optionally err.name) instead of the full error object, or explicitly redact sensitive fields before logging.

⚪ No debouncing on repeated badge refresh triggers

No debouncing on repeated badge refresh triggers

Action: Add a simple debounce/coalesce guard (pending-promise flag or short timer) around refreshPendingBadge() invocations from port lifecycle events.

🔵 Port-name-only gate for triggering badge refresh (pre-existing, now exercised more by new code)

Port-name-only gate for triggering badge refresh (pre-existing, now exercised more by new code)

Action: Validate chrome.runtime.MessageSender (sender.id === chrome.runtime.id, and origin/url checks) in the onConnect listener, and verify manifest.json externally_connectable does not expose this port to non-extension origins.

⚪ New internal dependency edge on @openvtc/pnm-core's pending-record schema without runtime validation

New internal dependency edge on @openvtc/pnm-core's pending-record schema without runtime validation

Action: Add a lightweight runtime shape check (e.g., zod/io-ts or manual typeof validation) on returned pending records before filtering, and pin/verify the @openvtc/pnm-core workspace version.

🧩 Affected Components

Component Impact Change What Changed
Extension Background Service Worker medium modified Added a badge-refresh side effect wired into service worker wake, and consent keepalive port connect/disconnect events.
Consent/Approval UX medium added New durable-state-derived visibility surface (toolbar badge) added alongside the existing popup-window approval prompt.
@openvtc/pnm-core dependency surface low modified New functional coupling introduced to listPendingInbound's return shape (isApprover field) and IndexedDBKVStore read semantics.

📁 File Classifications

packages/extension/src/background.ts

  • Type: security-critical

💡 Recommendations

  • MUST — Confirm that pending.ts (the durable-record writer, outside this diff's scope) validates mediator-message provenance/signatures before recording a pending-approval entry, and that the actual consent-approval decision flow independently re-validates requests rather than trusting listPendingInbound output as an authorization source (effort: medium)
    • The badge's trustworthiness is entirely dependent on the integrity of this upstream write path, which cannot be verified from within background.ts and is the most significant residual risk identified
  • SHOULD — Add a lightweight runtime shape/schema validation on records returned by listPendingInbound before filtering on isApprover (effort: small)
    • Prevents silent fail-open behavior (badge showing 0 pending when approvals actually exist) if the pnm-core package's record shape drifts in the future
  • SHOULD — Replace raw error object logging in the catch block with err.message/err.name only (effort: trivial)
    • Avoids potential leakage of internal state or sensitive record data into browser console logs that could later be captured by logging/crash-reporting integrations
  • SHOULD — Validate chrome.runtime.MessageSender (sender.id, and origin/url if applicable) in the CONSENT_KEEPALIVE_PORT onConnect listener, and verify manifest.json externally_connectable does not expose this port to non-extension origins (effort: small)
    • The pre-existing port-name-only gate now triggers more work per connection; strengthening this gate reduces exposure independent of this PR
  • CONSIDER — Add debounce/coalescing around refreshPendingBadge() invocations triggered by rapid port connect/disconnect cycling (effort: small)
    • Prevents unnecessary repeated concurrent IndexedDB reads and chrome.action calls under abusive or buggy rapid connection cycling, even though current impact is low
  • CONSIDER — Verify no other part of the extension (outside this diff) manages chrome.action badge text/color, to avoid last-writer-wins conflicts between this feature and any other badge consumer (effort: trivial)
    • Introduces a new consumer of the shared chrome.action badge state that could silently clobber or be clobbered by other badge-setting code

✅ Positive Observations

  • Feature is entirely read-only with respect to security-relevant state — no new writes, no new network calls, no new privileged API usage beyond low-privilege chrome.action badge APIs
  • Errors in the new code path are explicitly caught and logged rather than allowed to crash or block the security-critical consent keepalive wake path ('Never let a cosmetic surface break a wake path')
  • Design explicitly derives the UI signal from the same durable record the inbound path writes before acking the mediator, reducing state desync between backend record and UI, and is idempotent by design
  • Thoughtful, well-documented rationale comment explaining the design trade-off between fragile popup windows and durable-state-derived badges, aiding future maintainability
  • No breaking changes to existing port protocol or message contracts — the CONSENT_KEEPALIVE_PORT semantics are unchanged, only a new side effect was appended
  • Change is small, self-contained, and easy to review (single file, single new function, three call sites)

🛡️ STRIDE Threat Model

Identified Threats (10)

⚪ STRIDE-1: Unbounded IndexedDB Read Loop via onConnect Port Flooding in Background Service Worker

Field Detail
Category Denial of Service
Severity Medium
Likelihood Likely
CVSS 5.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-400,CWE-770
CAPEC CAPEC-125
OWASP A04:2021 - Insecure Design

Description: chrome.runtime.onConnect listener in background.ts allows a malicious or compromised content script to open/close CONSENT_KEEPALIVE_PORT connections in rapid succession due to unthrottled invocation of refreshPendingBadge() on every connect and disconnect event, resulting in repeated full IndexedDB scans and potential service worker resource exhaustion / battery-drain denial of service.

Evidence: packages/extension/src/background.ts:478-490

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== CONSENT_KEEPALIVE_PORT) return;
  void refreshPendingBadge();
  port.onDisconnect.addListener(() => {
    void refreshPendingBadge();
  });
});

Attack Scenario:

  1. An attacker-controlled or compromised web page/content script with access to chrome.runtime.connect({name: CONSENT_KEEPALIVE_PORT}) (any page the extension exposes externally_connectable to, or a compromised content script context) opens and immediately closes the port in a tight loop.
  2. Each onConnect fire triggers void refreshPendingBadge() (background.ts, added lines ~478-480), and each onDisconnect fires it again (line ~482-484).
  3. refreshPendingBadge() calls listPendingInbound(new IndexedDBKVStore()) which performs a full read/filter of the pending-inbound IndexedDB store on every invocation with no debounce, rate limit, or in-flight de-duplication.
  4. Repeated rapid connect/disconnect cycles cause concurrent overlapping IndexedDB transactions and repeated chrome.action.setBadgeText/setBadgeBackgroundColor calls, consuming CPU/IO and potentially starving the legitimate consent-approval keepalive path or delaying the service worker from processing real DIDComm messages.
  5. Sustained flooding can keep the service worker perpetually busy or repeatedly awakened, degrading extension responsiveness and battery life on the victim's device (self-DoS amplification since no external network egress occurs, but local resource contention still impacts consent-critical flows).

🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-002

  • Data Flows: pending-inbound IndexedDB read -> badge UI write

Preconditions: Attacker page or script has a means to call chrome.runtime.connect against this extension (matches externally_connectable config or attacker has compromised a content script), No rate limiting exists on onConnect/onDisconnect handling in the current code

Existing Controls: Port name gate (if (port.name !== CONSENT_KEEPALIVE_PORT) return;) restricts the code path to a specific, known channel name • refreshPendingBadge() wraps failures in try/catch so errors do not crash the worker

Recommended Mitigations: Add debounce/throttle logic (e.g., coalesce refresh calls within a short window) around refreshPendingBadge() invocations • Restrict externally_connectable / runtime messaging surface in manifest.json to only trusted origins • Cache pending-inbound results with a short TTL to avoid redundant IndexedDB scans on rapid successive triggers


⚪ STRIDE-2: Race Condition Between Concurrent refreshPendingBadge Invocations Causing Stale Badge State

Field Detail
Category Tampering, Repudiation
Severity Low
Likelihood Possible
CVSS 3.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-362,CWE-367
CAPEC CAPEC-26
OWASP A04:2021 - Insecure Design

Description: refreshPendingBadge in background.ts allows a TOCTOU-style race between overlapping async invocations due to lack of mutex/sequencing around the IndexedDB read and subsequent badge write, resulting in the badge displaying a stale or incorrect pending-approval count that could mislead a user's trust decision about outstanding DID approval requests.

Evidence: packages/extension/src/background.ts:460-473

async function refreshPendingBadge(): Promise<void> {
  try {
    const pending = await listPendingInbound(new IndexedDBKVStore());
    const waiting = pending.filter((p) => p.isApprover).length;
    await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });
    ...
  } catch (e

Attack Scenario:

  1. Multiple triggers (module-load, onConnect, onDisconnect, and future push-doorbell integration mentioned in comments) can call refreshPendingBadge() concurrently without any locking or sequencing token.
  2. Two overlapping calls read listPendingInbound() at different points in time (e.g., call A reads before a new pending request is durably written, call B reads after).
  3. Because chrome.action.setBadgeText calls are not ordered relative to their originating read, call A (with stale/lower count) may resolve and write the badge AFTER call B (with the current/higher count), overwriting the correct value with a stale one.
  4. The user observes a badge showing fewer (or zero) pending approvals than actually exist, potentially causing them to miss a pending DID approval request that requires their action, or conversely to believe an approval is still pending after it has been handled.
  5. Because there is no logging of which invocation set which badge value, this misleading state cannot be traced or disputed after the fact (repudiation of the badge's accuracy).

🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-002

  • Data Flows: pending-inbound IndexedDB read -> badge UI write

Preconditions: Multiple wake events occur within a short time window (plausible given comments referencing startup, consent port, and future push doorbell), IndexedDB write for a new pending record and a badge refresh trigger happen close in time

Existing Controls: Badge state is always derived from durable IndexedDB state rather than in-memory counters, bounding the maximum staleness window to roughly one wake cycle

Recommended Mitigations: Introduce a monotonic sequence number or generation counter so out-of-order badge writes can be detected and discarded • Serialize refreshPendingBadge invocations via a simple promise-chaining lock • Add debug logging correlating each badge write to the read snapshot used


⚪ STRIDE-3: Silent Failure Swallowing in refreshPendingBadge Enabling Undetected Badge Desynchronization

Field Detail
Category Denial of Service, Repudiation
Severity Low
Likelihood Possible
CVSS 2.6 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-755,CWE-390
CAPEC CAPEC-227
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: refreshPendingBadge in background.ts allows persistent badge desynchronization due to a catch-all try/catch that only logs a console.warn without any retry, alerting, or escalation mechanism, resulting in the security-relevant pending-approval indicator silently becoming permanently stale if IndexedDBKVStore or listPendingInbound throws (e.g., due to storage quota, corruption, or extension update race).

Evidence: packages/extension/src/background.ts:467-469

} catch (err) {
  console.warn("[pnm consent] could not refresh the pending badge:", err);
}

Attack Scenario:

  1. An attacker (or unrelated system fault) causes IndexedDB access to fail transiently or persistently — e.g., by exhausting browser storage quota, triggering a storage corruption bug, or racing an extension update that invalidates the DB connection.
  2. listPendingInbound(new IndexedDBKVStore()) throws inside refreshPendingBadge().
  3. The catch block only logs console.warn("[pnm consent] could not refresh the pending badge:", err) — a message visible only in the extension's DevTools console, which ordinary users never open.
  4. The badge is never updated (setBadgeText is never called in the failure path), so it silently retains its last-known value indefinitely, even if new legitimate approval requests arrive.
  5. A user relying on the badge count to know they have pending DID approvals is misled into believing no action is required, potentially missing a time-sensitive consent request (e.g., an attacker-initiated DID linkage or credential exchange request that requires their approval) without any visible signal of failure.

🔎 Threat Clue: Derived from COMP-001 via EP-002

  • Data Flows: pending-inbound IndexedDB read -> badge UI write

Preconditions: IndexedDB access transiently or persistently fails (quota, corruption, extension update race, storage API changes), User does not have DevTools open to observe the console.warn

Existing Controls: try/catch prevents the failure from crashing the service worker or breaking the wake path

Recommended Mitigations: On failure, set the badge to an explicit error state (e.g., a distinct icon/color) rather than leaving stale state unchanged • Implement bounded retry with backoff for transient IndexedDB errors • Surface a persistent, user-visible warning (e.g., extension icon overlay) if the badge subsystem fails repeatedly


⚪ STRIDE-4: Trust of isApprover Flag from Local IndexedDB Enabling Badge Spoofing via Store Tampering

Field Detail
Category Spoofing, Tampering
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-345,CWE-807
CAPEC CAPEC-176,CAPEC-141
OWASP A08:2021 - Software and Data Integrity Failures

Description: refreshPendingBadge in background.ts allows badge count spoofing due to unvalidated trust in the isApprover boolean field returned by listPendingInbound() from IndexedDB, resulting in an attacker with local storage write access (e.g., via a separate compromised extension, a DevTools-based attack, or a supply-chain-compromised dependency writing to the same origin's IndexedDB) being able to falsify the number of pending approvals shown to the user.

Evidence: packages/extension/src/background.ts:462-463

const pending = await listPendingInbound(new IndexedDBKVStore());
const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;

Attack Scenario:

  1. An attacker gains write access to the extension's IndexedDB store — for example via another malicious extension with unlimitedStorage/storage access, a supply-chain-compromised transitive dependency of @openvtc/pnm-core, or direct DevTools manipulation during a social-engineering session.
  2. The attacker inserts fabricated pending-inbound records with isApprover: true into the store consumed by listPendingInbound.
  3. On the next wake (module load, onConnect, onDisconnect), refreshPendingBadge() reads these records verbatim and computes waiting = pending.filter(p => p.isApprover).length without any integrity check (e.g., no signature verification, no cross-check against the pending.ts write path's provenance).
  4. chrome.action.setBadgeText/setBadgeBackgroundColor display a fabricated count and the alarming dark-red color (#8B1A1A), pressuring the user into opening the extension and approving what they believe are legitimate pending DID/DIDComm requests.
  5. If the extension's approval UI trusts the same unauthenticated local record when rendering the actual consent prompt, the attacker can potentially trick the user into approving an unintended DID linkage or credential exchange, escalating from local storage tampering to actual identity/consent compromise.

🔎 Threat Clue: Derived from COMP-001 via EP-002, EP-003

  • Data Flows: pending-inbound IndexedDB read -> badge UI write

Preconditions: Attacker has local write access to the browser's IndexedDB for this extension's origin (separate compromise required — not remotely exploitable via network), Approval UI (outside this diff) also trusts the same record without re-validating provenance/signature

Existing Controls: Badge is described as derived from 'the same durable record the inbound path writes before it acks the mediator', implying some upstream write discipline in pending.ts (not visible in this diff)

Recommended Mitigations: Cryptographically sign or MAC pending-inbound records at write time and verify integrity at read time in listPendingInbound() • Cross-validate isApprover claims against the DIDComm message's verified sender/recipient DID rather than trusting a stored boolean alone • Apply Chrome extension storage isolation best practices and minimize permissions requested by the extension to reduce cross-extension tampering risk


⚪ STRIDE-5: Supply Chain Compromise of @openvtc/pnm-core Enabling Arbitrary Code Execution in Privileged Background Context

Field Detail
Category Tampering, Elevation of Privilege
Severity High
Likelihood Unlikely
CVSS 8.2 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-1104,CWE-829
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: The newly added import of IndexedDBKVStore and listPendingInbound from @openvtc/pnm-core in background.ts allows full compromise of the extension's privileged service-worker context due to unconditional trust in an internal npm package with no visible integrity pinning or sandboxing shown in this diff, resulting in arbitrary code execution with access to all DIDComm keys, tokens, and consent flows if the package is compromised upstream.

Evidence: packages/extension/src/background.ts:11

import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core";

Attack Scenario:

  1. An attacker compromises the @openvtc/pnm-core package (e.g., via a maintainer account takeover, malicious version publish, or a compromised build pipeline), which this diff newly imports into background.ts (import { IndexedDBKVStore, listPendingInbound } from "@openvtc/pnm-core").
  2. The compromised package version is pulled in during the next npm install/build if there is no lockfile pinning or integrity hash verification enforced in CI.
  3. Malicious code inside IndexedDBKVStore or listPendingInbound executes within the extension's background service worker context — the same context that holds DIDComm keys, REST tokens, and orchestrates the consent flow described in the file header comments.
  4. The malicious code exfiltrates sensitive tokens/keys via any available channel (e.g., piggy-backing on legitimate network calls made elsewhere in background.ts, or writing to attacker-controlled storage) or silently approves/forges pending DID approvals by manipulating the very store this diff reads from.
  5. Because the service worker has the highest privilege level in the extension (manages both REST and DIDComm token flows), full compromise of identity and consent data results, extending far beyond the badge feature itself.

🔎 Threat Clue: Derived from COMP-001 via EP-001, EP-002

  • Data Flows: @openvtc/pnm-core import -> background service worker execution context

Preconditions: @openvtc/pnm-core or one of its transitive dependencies is compromised upstream, No subresource integrity / lockfile hash pinning / reproducible build verification is enforced in the build pipeline

Existing Controls: Package appears to be an internal, presumably first-party dependency, reducing likelihood relative to public third-party packages

Recommended Mitigations: Enforce lockfile integrity hashes (npm/yarn/pnpm) and verify them in CI for all internal and external dependencies • Apply Software Bill of Materials (SBOM) generation and dependency provenance verification (e.g., Sigstore/SLSA) for @openvtc/pnm-core releases • Adopt least-privilege module design: isolate storage/DID logic from token-handling logic so a compromised storage helper cannot reach token material • Pin and audit @openvtc/pnm-core releases with code review gating before consumption by the extension


⚪ STRIDE-6: Missing Origin Validation on onConnect Port Enabling Cross-Extension or Web Page Wake Triggering

Field Detail
Category Spoofing, Denial of Service
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-346,CWE-306
CAPEC CAPEC-194
OWASP A07:2021 - Identification and Authentication Failures

Description: chrome.runtime.onConnect listener in background.ts allows unauthenticated triggering of refreshPendingBadge() from any context able to open a port named CONSENT_KEEPALIVE_PORT due to the absence of sender/origin validation (only the port name string is checked), resulting in unauthorized repeated wake-up of the service worker and forced IndexedDB reads by any caller that knows the constant's value.

Evidence: packages/extension/src/background.ts:478-480

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== CONSENT_KEEPALIVE_PORT) return;
  void refreshPendingBadge();

Attack Scenario:

  1. CONSENT_KEEPALIVE_PORT is a fixed string constant compiled into the shipped extension bundle, discoverable by any party who inspects the extension's public source/bundle (Chrome extensions are typically not obfuscated against inspection).
  2. The onConnect listener validates only port.name, not port.sender (e.g., port.sender.id, port.sender.url, or port.sender.origin), per: if (port.name !== CONSENT_KEEPALIVE_PORT) return;.
  3. Any web page listed in externally_connectable in manifest.json (or another extension with access to chrome.runtime.connect targeting this extension's ID) can open a port with this exact name.
  4. Each such connection triggers refreshPendingBadge() and its associated IndexedDB read, without the caller needing any legitimate relationship to an actual consent flow.
  5. Combined with STRIDE-1, this enables trivially triggering the DoS/resource-exhaustion pattern from any page permitted to message the extension, since the port-name check alone does not authenticate the caller's intent or identity.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: chrome.runtime.onConnect -> refreshPendingBadge

Preconditions: manifest.json externally_connectable configuration permits some external origins to call chrome.runtime.connect (not shown in this diff, assumed plausible given consent-port architecture), CONSENT_KEEPALIVE_PORT constant value is discoverable via static analysis of the shipped bundle

Existing Controls: Port name string matching provides a minimal filter, preventing arbitrary named ports from triggering the listener body

Recommended Mitigations: Validate port.sender.id against the extension's own ID or an explicit allowlist before acting on the connection • Avoid exposing consent-related keepalive ports via externally_connectable if not strictly required • Add rate limiting per sender/origin on wake-triggering connections


⚪ STRIDE-7: Information Disclosure of Pending Approval Count via Ambient Badge UI Exposed to Shoulder-Surfers

Field Detail
Category Information Disclosure
Severity Low
Likelihood Unlikely
CVSS 2.1 CVSS:4.0/AV:P/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-200
CAPEC CAPEC-651
OWASP A01:2021 - Broken Access Control

Description: chrome.action.setBadgeText in refreshPendingBadge allows low-severity information disclosure of the count of pending DID approval requests to anyone with visual access to the browser toolbar due to the badge being rendered unconditionally without any user-configurable privacy setting, resulting in an observer learning that the user has outstanding identity/consent transactions.

Evidence: packages/extension/src/background.ts:464-466

await chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" });
if (waiting > 0) {
  await chrome.action.setBadgeBackgroundColor({ color: "#8B1A1A" });
}

Attack Scenario:

  1. refreshPendingBadge() unconditionally displays the numeric count of pending approver requests via chrome.action.setBadgeText({ text: waiting > 0 ? String(waiting) : "" }) on the visible browser toolbar icon.
  2. Any person with physical or remote screen-sharing visibility of the victim's browser (e.g., during a screen share, in a shared workspace, or shoulder-surfing) can observe that the user has one or more pending DID/DIDComm approval requests.
  3. This reveals metadata about the user's identity-wallet activity (e.g., that they are mid-way through a credential exchange or DID linkage) without requiring any interaction with the extension.
  4. Combined with timing/context (e.g., observing when the badge appears during a video call), an observer could infer sensitive business relationships or transaction timing patterns.

🔎 Threat Clue: Derived from COMP-001 via EP-003

  • Data Flows: pending-inbound count -> badge UI

Preconditions: Observer has visual or remote access to the victim's browser toolbar, Extension is pinned/visible in the toolbar

Existing Controls: None specific to visibility; this is an inherent trade-off of any badge-based UI

Recommended Mitigations: Provide a user setting to disable the numeric badge or replace it with a generic non-numeric indicator • Avoid rendering the badge during screen-sharing sessions if detectable via browser APIs


⚪ STRIDE-8: Repudiation of Badge State Changes Due to Absence of Audit Logging for Pending-Approval Transitions

Field Detail
Category Repudiation
Severity Low
Likelihood Possible
CVSS 2.8 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N
Residual Severity Low
CWE CWE-778
CAPEC CAPEC-268
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: refreshPendingBadge in background.ts allows undetectable and unauditable transitions of the pending-approval badge state due to the complete absence of structured audit logging (only a best-effort console.warn on error exists), resulting in an inability to forensically reconstruct when and why a user was or was not shown a pending-approval indicator during a security incident investigation.

Evidence: packages/extension/src/background.ts:460-470

async function refreshPendingBadge(): Promise<void> {
  try {
    ...
  } catch (err) {
    console.warn("[pnm consent] could not refresh the pending badge:", err);
  }
}

Attack Scenario:

  1. During an incident investigation (e.g., a user disputes having approved a malicious DID linkage, claiming they never saw a pending notification), an investigator attempts to reconstruct the badge's historical state.
  2. refreshPendingBadge() has no persistent, structured logging of when it ran, what count it computed, or what badge value it set — only a console.warn on the error path, which is ephemeral (lost on service worker restart) and not exported anywhere.
  3. Because Chrome extension service workers are frequently torn down and restarted, even the ephemeral console output is unlikely to survive to the point of investigation.
  4. The investigator cannot determine whether the badge correctly reflected the pending count at the time in question, undermining any claim (by the user or the vendor) about what was or was not visibly communicated.
  5. This gap allows either party to plausibly deny knowledge of a pending approval notification state, weakening non-repudiation guarantees for consent-related UX.

🔎 Threat Clue: Derived from COMP-001 via EP-002

  • Data Flows: pending-inbound IndexedDB read -> badge UI write

Preconditions: A dispute or investigation arises regarding whether a user was shown a pending-approval indicator, No external logging/telemetry pipeline captures badge state transitions

Existing Controls: console.warn exists for the error path only, providing partial forensic value if DevTools happened to be open and captured

Recommended Mitigations: Emit structured, timestamped audit events (locally persisted or sent to a telemetry backend with user consent) for each badge state transition • Correlate badge audit events with the underlying pending-inbound record IDs for full traceability


⚪ STRIDE-9: Unvalidated Filter Predicate Type Assumption Enabling Type Confusion in isApprover Filtering

Field Detail
Category Tampering, Denial of Service
Severity Low
Likelihood Possible
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-843,CWE-20
CAPEC CAPEC-153
OWASP A08:2021 - Software and Data Integrity Failures

Description: The filter predicate in refreshPendingBadge allows type confusion due to the TypeScript type annotation { isApprover: boolean } being a compile-time-only assertion with no runtime validation of the actual shape returned by listPendingInbound(), resulting in incorrect badge counts or a runtime exception if the underlying record schema drifts (e.g., isApprover becomes a string, undefined, or the field is renamed in @openvtc/pnm-core) without a corresponding type update in background.ts.

Evidence: packages/extension/src/background.ts:463

const waiting = pending.filter((p: { isApprover: boolean }) => p.isApprover).length;

Attack Scenario:

  1. pending.filter((p: { isApprover: boolean }) => p.isApprover) casts the runtime object shape without validation — TypeScript types are erased at runtime and provide zero protection against schema drift or malformed data from IndexedDB.
  2. If @openvtc/pnm-core changes the shape of pending records (e.g., a future version renames isApprover to isApproverRole, or a partially-written/corrupted IndexedDB record has isApprover as undefined or a truthy string like "false"), the filter silently misbehaves.
  3. A truthy-string edge case (e.g., isApprover: "false") would cause the filter to treat a non-approver record as an approver, inflating the badge count and potentially causing the user to believe they have approval authority over a request they should not act on.
  4. Alternatively, a schema drift causing pending.filter to throw (e.g., if pending itself is not an array due to an upstream serialization bug) propagates to the catch block, silently disabling the badge feature per STRIDE-3.
  5. Because there is no runtime schema validation (e.g., zod/io-ts) at the IndexedDB boundary, this weakness would only surface in production after a version mismatch between @openvtc/pnm-core and the extension bundle.

🔎 Threat Clue: Derived from COMP-001 via EP-002

  • Data Flows: pending-inbound IndexedDB read -> filter -> badge UI write

Preconditions: Schema drift between @openvtc/pnm-core's pending-record shape and the extension's compiled expectations, or corrupted/partially-written IndexedDB records

Existing Controls: TypeScript compile-time typing catches drift only if both packages are rebuilt and type-checked together; monorepo tooling may partially mitigate this

Recommended Mitigations: Add runtime schema validation (e.g., zod) for records returned by listPendingInbound() before filtering • Version the pending-record schema explicitly and reject/migrate records that do not match the expected version • Add unit tests covering malformed/missing isApprover field scenarios


⚪ STRIDE-10: Unawaited Top-Level Promise Rejection at Module Load Enabling Silent Service Worker Instability

Field Detail
Category Denial of Service, Repudiation
Severity Low
Likelihood Unlikely
CVSS 2.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-755
CAPEC CAPEC-227
OWASP A04:2021 - Insecure Design

Description: The top-level void refreshPendingBadge(); call in background.ts allows an unhandled rejection edge case due to the void operator suppressing any promise rejection warnings at the module scope during every service worker cold start, resulting in potential silent failure signals being lost precisely at startup — the moment most likely to reveal environment-level IndexedDB initialization races.

Evidence: packages/extension/src/background.ts:477

void refreshPendingBadge();

Attack Scenario:

  1. Every time the Manifest V3 service worker cold-starts (browser restart, extension update, forced eviction after idle timeout), the module-level statement void refreshPendingBadge(); executes immediately.
  2. At this exact moment, IndexedDBKVStore may race against the browser's IndexedDB subsystem still initializing post-restart, a known source of flaky failures in MV3 service workers per the file's own comments about 'a live service worker... on a runtime that is free to kill either.'
  3. Although refreshPendingBadge internally catches errors, the void operator at the call site additionally signals to any static analysis or future refactor that the promise result is intentionally discarded, making it easy for a future contributor to remove the internal try/catch without noticing the caller provides no safety net.
  4. If the internal try/catch were ever removed or bypassed (e.g., by a future refactor moving IndexedDB access outside the try block), the rejection at this top-level void call would become a genuinely unhandled promise rejection, which in some browser/MV3 environments can log noisy errors or, in stricter contexts, contribute to service worker termination heuristics.
  5. This creates a latent maintenance-driven denial-of-service risk that is not exploitable today but represents a fragile invariant (the badge always being safe) that depends entirely on the internal catch never being removed or restructured.

🔎 Threat Clue: Derived from COMP-001 via EP-002

  • Data Flows: module load -> refreshPendingBadge invocation

Preconditions: A future code change removes or weakens the internal try/catch inside refreshPendingBadge without updating the top-level call site, IndexedDB initialization race occurs during MV3 service worker cold start

Existing Controls: Internal try/catch inside refreshPendingBadge fully absorbs all current failure modes, per the code's own explicit design comment

Recommended Mitigations: Add a lint rule or code comment enforcing that refreshPendingBadge must never be modified to remove its internal try/catch • Add a global unhandledrejection listener in the service worker as defense-in-depth against any future regression



🍝 PASTA Threat Model

Application Purpose

A Chrome browser extension (Manifest V3) that manages verifiable trusted agent (VTA) DID/DIDComm identity flows, mediating REST-based logins and DIDComm-based credential/consent exchanges via a background service worker and offscreen document, providing business value as a decentralized identity wallet for regulated or trust-sensitive digital interactions.

Inherent Risks

  • Manifest V3 service workers are ephemeral and can be terminated/restarted unpredictably, complicating reliable state and UI consistency
  • Local IndexedDB storage of consent/identity-related state is inherently trusted without cryptographic provenance in the reviewed code path
  • The extension depends on an internal package (@openvtc/pnm-core) whose supply-chain integrity controls are not visible in this diff
  • Browser extension messaging surfaces (onConnect) are exposed to any context permitted by the manifest's externally_connectable configuration

Objectives

Risk: Limit denial-of-service and spoofing exposure introduced by new wake-triggering code paths added in this diff
Business: Provide a reliable, trustworthy consent and badge indicator so users never miss a pending identity approval request
Security: Protect the integrity and confidentiality of DIDComm identity and consent data flowing through the background service worker
Financial: Avoid costs associated with security incidents involving unauthorized DID approvals or credential exchanges
Compliance: Maintain auditability of consent-related UI state for potential trust/identity assurance compliance frameworks
Functional: Accurately reflect the count of pending inbound DID approval requests requiring user action via a persistent badge
Operational: Ensure the badge refresh logic is resilient to Manifest V3 service worker teardown/restart cycles

Business Impact Analysis (1)

BIA-1: Pending DID Approval Notification Delivery (High)

The end-to-end process of durably recording an inbound DID/DIDComm approval request and reliably surfacing it to the user via the extension badge until they act on it.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:05 hours

  • Stakeholders: End Users / Extension Development Team / Mediator Service Operators / Relying Parties Requesting DID Approval
  • Dependencies: @openvtc/pnm-core Package / Chrome Extension Action API / IndexedDB Storage / Service Worker Runtime
  • Disruptions: Service worker fails to wake or refresh badge due to browser resource eviction / IndexedDB read failure silently disables badge updates / Malicious port flooding degrades badge refresh performance / Tampered IndexedDB records inflate or falsify pending counts
  • Impacts: User misses a time-sensitive DID approval request, leading to a missed business transaction / User is misled into approving a request they did not actually have pending, risking unauthorized credential/identity linkage / Reputational damage if the wallet extension is perceived as unreliable for consent notifications

Technical Scope

Roles (2): RO-1 Wallet End User · RO-2 Extension Maintainer

Actors (3): AC-1 End User · AC-2 Background Service Worker Process · AC-3 @openvtc/pnm-core Maintainers

Entry Points (2): EP-1 onConnect CONSENT_KEEPALIVE_PORT Listener · EP-2 Module-Load Badge Refresh Trigger

Threat Actors (3): TA-1 Malicious Web Page Operator · TA-2 Local Malware / Compromised Co-Installed Extension · TA-3 Supply Chain Attacker

Infrastructure (1): IF-1 End-User Browser Environment

Trust Boundaries (3): TB-1 Browser Extension Runtime Boundary · TB-2 Local Device Storage Boundary · TB-3 External Package Registry Boundary

External Entities (2): EE-1 Web Page / Content Script Caller · EE-2 npm Registry / Build Pipeline

System Components (4): SC-1 Background Service Worker (background.ts) · SC-2 IndexedDB Pending-Inbound Store · SC-3 @openvtc/pnm-core Package · SC-4 Offscreen Document / Consent UI

Resources And Assets (2): RA-1 Pending Inbound Approval Records · RA-2 Extension Action Badge State

Technologies And Dependencies (3): TD-1 Chrome Extension Manifest V3 · TD-2 @openvtc/pnm-core · TD-3 IndexedDB (Browser Storage API)

Use Cases (1)

  • Pending DID Approval Badge Notification: The extension's background service worker periodically refreshes a toolbar badge reflecting the count of pending inbound DID approval requests the user still needs to act on, triggered by service work

📋 Risk Registry (4)

ID Title Severity Residual Priority Effort
RISK-1 Unauthenticated wake-triggering surface enables resource-exhaustion denial of service against the consent badge subsystem Medium Low Short-Term Low
RISK-2 Unauthenticated local storage trust enables spoofing of pending approval counts and potential misled consent decisions Medium Medium Short-Term Medium
RISK-3 Lack of supply-chain integrity controls for the internal @openvtc/pnm-core dependency risks full compromise of the privileged background context High Medium Medium-Term High
RISK-4 Silent failure handling and absent audit logging undermine detection and forensic reconstruction of badge/consent notification failures Low Low Medium-Term Medium

⚔️ Attack Scenarios (3)

SC-1: Background Service Worker

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: Background Service Worker" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE400@{ shape: rect, label: "CWE-400: Uncontrolled Resource Consumption" }
    CWE346@{ shape: rect, label: "CWE-346: Origin Validation Error" }
    CWE362@{ shape: rect, label: "CWE-362: Race Condition" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC125@{ shape: rect, label: "CAPEC-125: Flooding" }
    CAPEC194@{ shape: rect, label: "CAPEC-194: Fake the Source of Data" }
    CAPEC26@{ shape: rect, label: "CAPEC-26: Leveraging Race Conditions" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S1@{ shape: rect, label: "STRIDE-1: Unbounded IndexedDB Read Loop<br><i>Medium / Likely</i>" }
    S6@{ shape: rect, label: "STRIDE-6: Missing Origin Validation<br><i>Medium / Possible</i>" }
    S2@{ shape: rect, label: "STRIDE-2: Race Condition in Badge Refresh<br><i>Low / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Web Page Operator<br><i>Degrade reliability via crafted port connections</i>" }
  end
  SC1 --> CWE400
  SC1 --> CWE346
  SC1 --> CWE362
  CWE400 --> CAPEC125
  CWE346 --> CAPEC194
  CWE362 --> CAPEC26
  CAPEC125 --> S1
  CAPEC194 --> S6
  CAPEC26 --> S2
  S1 --> TA1
  S6 --> TA1
  S2 --> TA1
  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:#FF0000,stroke-width:2px
  linkStyle 9 stroke:#FF0000,stroke-width:2px
  linkStyle 10 stroke:#FF0000,stroke-width:2px
  linkStyle 11 stroke:#FF0000,stroke-width:2px
Loading

SC-2: IndexedDB Pending-Inbound Store

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC2@{ shape: rect, label: "SC-2: IndexedDB Pending-Inbound Store" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
    CWE843@{ shape: rect, label: "CWE-843: Type Confusion" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC176@{ shape: rect, label: "CAPEC-176: Configuration/Environment Manipulation" }
    CAPEC153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S4@{ shape: rect, label: "STRIDE-4: Badge Spoofing via Store Tampering<br><i>Medium / Possible</i>" }
    S9@{ shape: rect, label: "STRIDE-9: Type Confusion in isApprover Filtering<br><i>Low / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: Local Malware / Compromised Extension<br><i>Tamper with local records to spoof state</i>" }
  end
  SC2 --> CWE345
  SC2 --> CWE843
  CWE345 --> CAPEC176
  CWE843 --> CAPEC153
  CAPEC176 --> S4
  CAPEC153 --> S9
  S4 --> TA2
  S9 --> TA2
  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
Loading

SC-3: @openvtc/pnm-core Package

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC3@{ shape: rect, label: "SC-3: @openvtc/pnm-core Package" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE1104@{ shape: rect, label: "CWE-1104: Use of Unmaintained Third-Party Components" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC538@{ shape: rect, label: "CAPEC-538: Open-Source Library Manipulation" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S5@{ shape: rect, label: "STRIDE-5: Supply Chain Compromise of pnm-core<br><i>High / Unlikely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA3@{ shape: rect, label: "TA-3: Supply Chain Attacker<br><i>Compromise pnm-core for code execution</i>" }
  end
  SC3 --> CWE1104
  CWE1104 --> CAPEC538
  CAPEC538 --> S5
  S5 --> TA3
  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
Loading

📊 Risk Summary

Total Threats: 10

By Severity: Low: 6 · High: 1 · Medium: 3

By Category: Unknown: 10

🎯 Attack Surface

Kill Chain 1: An attacker able to reach the extension's messaging surface (via a page permitted in externally_connectable or a compromised content script) opens and rapidly closes ports named CONSENT_KEEPALIVE_PORT (STRIDE-6, missing sender validation), each triggering refreshPendingBadge() and a full IndexedDB scan (STRIDE-1), which can degrade service worker responsiveness precisely when legitimate consent approvals need timely processing, creating a self-inflicted availability risk with no external network egress required. Kill Chain 2: An attacker who first achieves local code execution or storage access (e.g., via a co-installed malicious extension or compromised dependency) tampers directly with the IndexedDB pending-inbound store, injecting fabricated isApprover records (STRIDE-4); combined with the unvalidated runtime type assumptions in the filter predicate (STRIDE-9), this can inflate the badge count or force silent filtering failures (STRIDE-3), manipulating the user's perception of pending approvals and potentially steering them toward approving a request that does not correspond to a legitimate DIDComm interaction. Kill Chain 3: The most severe chain begins upstream of this diff: if @openvtc/pnm-core is compromised via supply-chain attack (STRIDE-5), the malicious code executes directly inside the same privileged background service worker context that this diff newly imports from, giving the attacker a foothold to both forge pending-inbound records at the source

🛡️ Risk Mitigation Strategy

Priority 1 (Short-Term): Close the two cheapest, highest-leverage gaps introduced directly by this diff — add sender/origin validation to the onConnect listener (beyond the existing port-name check) and introduce debounce/rate-limiting around refreshPendingBadge to eliminate the resource-exhaustion and spoofed-trigger vectors (STRIDE-1, STRIDE-6) at low implementation cost. Priority 2 (Short-Term to Medium-Term): Strengthen the integrity of the data trusted by the badge subsystem by adding runtime schema validation and provenance verification (e.g., signing) for pending-inbound records, directly addressing the spoofing and type-confusion risks (STRIDE-4, STRIDE-9) that could otherwise mislead users into unsafe consent decisions — this is the highest business-impact gap given the extension's role as an identity/consent tool. Priority 3 (Medium-Term): Improve observability and failure handling for the badge feature by replacing silent console.warn-only error handling with a visible degraded-state indicator and structured audit logging, closing the repudiation and detection gaps (STRIDE-2, STRIDE-3, STRIDE-8) that currently leave both users and investigators without reliable signals when the subsystem misbehaves. Priority 4 (Medium-Term to Long-Term): Address the systemic supply-chain risk represented by the unpinned trust extended to @openvtc/pnm-core (STRIDE-5) through lockfile integrity verification, dependency provenance attestation, and architectural separation between storage utilities and token-handling logic — this is the highest-severity but lowest-likelihood risk and should be pursued alongside broader organizational supply-chain security initiatives rather than as an isolated fix to this diff.


Generated by Agentic Sec — Threat Model & Affect Analysis Agent


🔧 What to do

# Action
1 📥 Download attached reports and review the findings and threat model
2 🤖 Feed reports to your IDE copilot for fixes or security hardening suggestions
3 🛡️ Review threat model for potential risks and recommended countermeasures
4 🆘 Questions? Reach out to the Security team

🛡️ Agentic Sec — AI Security Validation Agent

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