fix: stop asking a third time to publish a change already approved twice - #154
Conversation
A consent-gated DID edit prompted the operator three times: the worker-mode confirm, the approval ceremony on their device, and then the *same* worker-mode confirm again when the page replayed its pinned re-submit. The third prompt asks nothing new. By the time it fires the human has approved that exact payload twice — once here, once on the approving device, which showed the VTA's dry-run effects and a match code to compare — and the VTA is holding a single-use grant bound to the payload's digest. It is the same question, asked again on the weaker of the two surfaces. That is not free. The VTA's own `policy_gate.rs` states the cost: consent designs die to habituation long before they die to cryptography. Three prompts an edit, one of them uninformative, is how a person is trained to click through the one that matters. The gate itself is unchanged and still un-skippable — its reason holds, and is unrelated: with `policy.enforcement` off it is the only thing between an arbitrary page and an arbitrary task. What changes is that one replay is recognised as the completion of a ceremony rather than a new request. The exemption requires all three of: the same origin and byte-identical params; a prior `consentRequired` refusal for them, so only a task that did *not* run is ever tracked; and a `task-consent/granted` since relayed, matched on the VTA's own `payloadDigest`. It is single-use and expires with the grant it depends on (600 s, the VTA's `GRANT_TTL_SECS`). The digest is never recomputed here. It comes off the wire twice — from the VTA's refusal and from its granted notice — so this adds no second implementation of a consensus-critical hash to drift out of step. That mattered: the digest construction is domain-tagged, length-prefixed and JCS-canonical, and a divergent copy would fail as a mismatch nobody sees. Every failure mode falls back to prompting. A key that does not match, a grant that never arrives, a page that re-serialises its payload between submits, a service-worker restart that empties the in-memory ledger — all show the confirm again, which is the direction a mistake should fail in. The state machine is a pure module so it can be tested without `chrome`, following `sites-model.ts`. Eleven tests, and most of them are about what must *not* be exempt: another payload from the same origin, the same payload from another origin, a grant for an unrelated digest, a grant that arrives before any refusal, a re-refused entry, and anything past the TTL. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review3 AI-confirmed issues. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #154
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 2 · findings: 4
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (3)🟡 Consent-replay ledger trusts wire-supplied payloadDigest without independent verification
🧠 AI Triage:
Summary: The extension takes the payloadDigest field directly from the offscreen/VTA response and stores it as-is to later arm a consent-bypass exemption, rather than independently computing or cryptographically validating that digest against the actual request payload. This design choice — justified in code comments as avoiding a second, drift-prone hash implementation — means the integrity of the entire replay-exemption mechanism rests on the trustworthiness of a value it does not verify. 📝 Description: If an attacker can influence the response value, they control the trust anchor for the entire exemption mechanism, enabling the STRIDE-1/VULN-001 bypass chain to succeed with an attacker-chosen digest rather than needing to guess a legitimate one. 🧪 Proof of Concept: The comment block explicitly states the digest is 'Taking it from the wire rather than recomputing it,' which is a deliberate trust decision. If the offscreen-to-VTA channel or the offscreen document's message construction can be influenced (e.g., by a separate vulnerability in the offscreen document or a supply-chain compromise of the VTA), the extension has no independent means to detect a digest that does not correspond to the actual payload. Vulnerable lines: 1660, 1675 🔁 Reproduction Steps:
🔎 Evidence: 💥 Impact: If an attacker can influence the response value, they control the trust anchor for the entire exemption mechanism, enabling the STRIDE-1/VULN-001 bypass chain to succeed with an attacker-chosen digest rather than needing to guess a legitimate one. 🧭 Reachability:
⚖️ Triage Factors:
Attack scenario: The extension trusts an unverified digest value from the offscreen/VTA response as the anchor for its consent-bypass ledger, so any influence over that response value lets an attacker control what will later arm the exemption. 🔧 Remediation:
Cross-checking the wire-supplied digest against a locally computed canonical digest (using the same, shared algorithm/spec as the VTA) ensures the value used to arm the exemption genuinely corresponds to the request payload, removing the single point of trust in an unverified wire value. If full recomputation is infeasible, at minimum require the digest to be a signed/MACed value verifiable with a key trusted by the extension. Vulnerable code: Secure code: Additional recommendations:
🟡 Consent replay exemption keyed only on digest allows cross-origin/cross-request confusion if digest collides or is attacker-influenced
🧠 AI Triage:
📝 Description: The consent bypass logic trusts a digest received from the offscreen/runtime message channel to arm a replay exemption, skipping the explicit user consent prompt on a subsequent identical request. 🌱 Root Cause: recordGranted() is invoked from a broadcast listener triggered by any message of type RUNTIME_EMIT_WALLET_EVENT with event 'consentgranted', and the digest used to arm the exemption is taken directly from message detail without independent verification that it originated from a legitimately signed VTA notice within this code path. 🔎 Evidence: 🎯 Attack Scenario: If any code path (extension bug, compromised offscreen document, or a future change) can emit a RUNTIME_EMIT_WALLET_EVENT message with event 'consentgranted' and an attacker-guessable or replayed payloadDigest, the background worker will arm an exemption that allows a subsequent request to skip the user consent popup entirely, effectively achieving privilege escalation over the human-in-the-loop control.
🟡 Missing audit logging on consent-exemption (bypass) code path weakens repudiation defenses
🧠 AI Triage:
Summary: There is no logging, telemetry, or user-visible indicator distinguishing a task that executed because the interactive consent prompt fired versus one that executed because the one-time replay exemption was consumed. This weakens the ability to investigate disputes or detect abuse of the exemption mechanism (including exploitation of VULN-001/VULN-002). 📝 Description: In the event of a dispute or forensic investigation ('I never approved this task'), there is no local evidence to determine whether the action was interactively approved or auto-exempted, hampering incident response for any successful exploitation of VULN-001/VULN-002. 🧪 Proof of Concept: The exemption branch (the implicit 'else' of the if-guard) contains no logging statement at all, so both the true-exemption and any bypass-abuse paths are indistinguishable in the extension's operational history from a normal approval flow. Vulnerable lines: 1645, 1660 🔁 Reproduction Steps:
🔎 Evidence: 💥 Impact: In the event of a dispute or forensic investigation ('I never approved this task'), there is no local evidence to determine whether the action was interactively approved or auto-exempted, hampering incident response for any successful exploitation of VULN-001/VULN-002. 🧭 Reachability:
🔧 Remediation:
Vulnerable code:
Generated by Agentic Sec — AI Security Validation Agent Details🛡️ Threat Model & Affect Analysis — PR #154
📋 Affect AnalysisChange SummaryThis PR modifies the mandatory user-consent gate for VTA task requests in the background service worker to add a single-use, time-bounded exemption path: if a request was previously refused with Diff: +168 / -8 lines
|
| Component | Impact | Change | What Changed |
|---|---|---|---|
| Background Service Worker Consent Gate (handleRequestTask) | critical | modified | The previously unconditional requestConsent() prompt for every VTA task request is now conditionally skipped when `consentReplays.consumeI |
| ConsentReplayLedger (new security primitive) | critical | new | A brand-new, in-memory, bounded, TTL-based ledger correlating consent refusals with subsequent grants to authorize exactly one bypass of the |
| Wallet Event Broadcast Path | medium | modified | The consentgranted event handler now performs a ledger-arming side effect (recordGranted) before forwarding the same event detail (includi |
📁 File Classifications
packages/extension/src/background.ts
- Type: security
packages/extension/src/consent-replay.ts
- Type: security
packages/extension/tests/consent-replay.test.mts
- Type: test
🛡️ STRIDE Threat Model
Identified Threats (10)
🟠 STRIDE-1: Consent Bypass via Digest Trust in ConsentReplayLedger
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Elevation of Privilege |
| Severity | High |
| Likelihood | Likely |
| CVSS | 8.2 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-345,CWE-290,CWE-863 |
| CAPEC | CAPEC-60,CAPEC-115 |
| OWASP | A01:2021 - Broken Access Control, A07:2021 - Identification and Authentication Failures |
Description: handleRequestTask in background.ts allows consent bypass via forged or replayed payloadDigest values due to the offscreen document/VTA response being trusted without independent recomputation, resulting in unauthorized task execution without a fresh user consent prompt.
Evidence: packages/extension/src/background.ts:1639-1680
const key = replayKey(req.origin, req.params);
if (!consentReplays.consumeIfArmed(key)) {
const approved = await requestConsent({...});
if (!approved.approved) return { ok: false, error: "user denied the request" };
}
Attack Scenario:
- Attacker-controlled or compromised VTA (or a MITM on the offscreen->VTA channel) crafts a
consentRequiredresponse with an attacker-chosenpayloadDigeststring, sent back throughchrome.runtime.sendMessagetohandleRequestTaskin background.ts. handleRequestTaskcallsconsentReplays.recordConsentRequired(key, digest)using the attacker-influenced digest verbatim, per the code comment 'Taking it from the wire rather than recomputing it'.- The same or colluding party later triggers
RUNTIME_EMIT_WALLET_EVENTwithevent: 'consentgranted'anddetail.payloadDigestmatching the previously recorded digest — this handler in thechrome.runtime.onMessage.addListenerblock callsconsentReplays.recordGranted(digest)without verifying the grant genuinely originated from the enrolled VTA's authenticated channel path beyond the offscreen 'inbound path' assumption. - Because
key = replayKey(req.origin, req.params)only binds origin+params (not the digest's provenance or cryptographic authenticity), any page atreq.originthat resubmits the identicalparamsnow hasconsumeIfArmed(key)return true. handleRequestTaskskipsrequestConsent(...)entirely and proceeds toensureOffscreenDocument()and dispatch ofOFFSCREEN_REQUEST_TASK, executing the VTA task with no human-in-the-loop confirmation.- Repeat for additional origins/params pairs if the attacker can trigger fabricated consentRequired/consentgranted event pairs multiple times, since the ledger is bounded only by
maxEntries(LRU eviction), not per-origin quotas.
🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-001, EP-002
- Data Flows: req.origin -> replayKey -> ConsentReplayLedger, OFFSCREEN_TARGET response -> recordConsentRequired, RUNTIME_EMIT_WALLET_EVENT -> recordGranted
Preconditions: Attacker can influence or spoof messages on the internal chrome.runtime.onMessage channel that emits RUNTIME_EMIT_WALLET_EVENT (e.g., via a compromised or malicious offscreen document, or a bug allowing content scripts to post to the extension's runtime message bus)., The consent-replay.ts implementation (not visible in this diff) does not perform any cryptographic verification of the payloadDigest's origin or signature.
Existing Controls: Ledger is in-memory and reset on service-worker restart. • Exemption is single-use (consumed on first successful replay). • Grant TTL (600s) limits window of exploitability. • replayKey binds origin and full params (with tested separator-injection resistance).
Recommended Mitigations: Recompute the payload digest locally instead of trusting the wire value, or require the digest to include a MAC/signature verifiable against the VTA's known public key. • Restrict which senders (chrome.runtime.MessageSender) may emit RUNTIME_EMIT_WALLET_EVENT with a consentgranted event to the offscreen document only, using sender.id/sender.url checks. • Add per-origin rate limiting/quota on ledger entries to prevent replay-flood scenarios. • Log all consent exemptions consumed for audit/detective purposes.
🟠 STRIDE-2: Unauthenticated Sender Spoofing of RUNTIME_EMIT_WALLET_EVENT
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Elevation of Privilege |
| Severity | High |
| Likelihood | Likely |
| CVSS | 7.7 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-346,CWE-306 |
| CAPEC | CAPEC-148,CAPEC-668 |
| OWASP | A07:2021 - Identification and Authentication Failures, A01:2021 - Broken Access Control |
Description: chrome.runtime.onMessage listener for RUNTIME_EMIT_WALLET_EVENT in background.ts allows message spoofing due to missing sender verification (sender.id/sender.url) on the consentgranted event, resulting in arbitrary arming of the consent-replay exemption ledger.
Evidence: packages/extension/src/background.ts:2517-2528
if (m.event === "consentgranted") {
const digest = m.detail?.payloadDigest;
if (typeof digest === "string" && digest) consentReplays.recordGranted(digest);
}
Attack Scenario:
- The listener registered via
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {...})in background.ts checks only(message as {type?:string})?.type === RUNTIME_EMIT_WALLET_EVENT— it does not validatesender.id,sender.url, orsender.origin. - Per EP-002 recon (auth_required: false), any extension component or, depending on manifest
externally_connectableconfiguration, potentially an external page can dispatch a message of this type. - Attacker crafts
{ type: RUNTIME_EMIT_WALLET_EVENT, event: 'consentgranted', detail: { payloadDigest: '<guessed-or-leaked-digest>' } }and sends it viachrome.runtime.sendMessagefrom any context able to reach the extension's message bus. - The handler unconditionally calls
consentReplays.recordGranted(digest)for any string digest, arming the ledger entry keyed by that digest without confirming the message truly originated from 'the offscreen inbound path' as the code comment assumes. - Combined with STRIDE-1, this allows an attacker to arm exemptions without any real consent grant ever occurring, provided they can guess or obtain the digest value (which is only a
payloadDigest, not cryptographically bound to sender identity).
🔎 Threat Clue: Derived from COMP-001 via EP-002
- Data Flows: chrome.runtime.onMessage -> RUNTIME_EMIT_WALLET_EVENT -> consentReplays.recordGranted
Preconditions: Attacker-controlled code (extension content script, malicious extension, or malformed externally_connectable page) can call chrome.runtime.sendMessage against this extension's ID., Digest value is obtainable or guessable (e.g., leaked via logs, other messages, or low entropy).
Existing Controls: recordGranted requires a matching digest previously seen via recordConsentRequired — reduces blind spoofing effectiveness. • Single-use exemption limits repeat abuse per key.
Recommended Mitigations: Validate sender.id === chrome.runtime.id and restrict to the extension's own offscreen document URL before processing consentgranted events. • Use a dedicated internal-only messaging channel (e.g., chrome.runtime.connect with a private port) instead of the shared onMessage bus for offscreen-to-background trust signals. • Increase digest entropy/unpredictability and bind it cryptographically to the specific consent ceremony instance.
🟡 STRIDE-3: TOCTOU Race Between recordConsentRequired and recordGranted Arming
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-367,CWE-697 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: The two-step ledger update sequence in background.ts (recordConsentRequired then recordGranted, from two different async message handlers) allows a time-of-check-to-time-of-use race due to lack of atomic transaction across the two chrome.runtime message flows, resulting in a page resubmitting a request mid-race to obtain an unintended exemption.
Evidence: packages/extension/src/background.ts:1650-1652
const key = replayKey(req.origin, req.params);
if (!consentReplays.consumeIfArmed(key)) { ... }
Attack Scenario:
- Page A submits a task request at time T0; handleRequestTask records
consentRequiredwith digest D viaconsentReplays.recordConsentRequired(key, D). - Before the legitimate consentgranted event for D arrives, the same page rapidly resubmits an equivalent request with slightly different params intended to collide on the same key due to potential key-normalization gaps (e.g., object key ordering, extra whitespace) not fully covered by the visible test suite.
- If
replayKeynormalization is imperfect (untestable here since consent-replay.ts implementation is not in scope), two logically different requests could map to the same ledger key. - When the legitimate grant callback fires and calls
recordGranted(D), both the original and the race-injected request become eligible forconsumeIfArmed. - Attacker's resubmitted request rides through the exemption without a fresh consent screen, executing an operation the user never explicitly reviewed for that exact resubmission.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: handleRequestTask -> ConsentReplayLedger keyed state
Preconditions: Imperfect canonicalization in replayKey/JSON serialization of params., Attacker can trigger rapid successive requests from the same origin.
Existing Controls: Tests confirm key separates origin from params and resists separator injection. • Single-use exemption limits blast radius to one bypass per arming.
Recommended Mitigations: Use canonical, deterministic JSON serialization (sorted keys) for params digesting in replayKey. • Bind the exemption to a request-specific nonce generated at request time rather than solely origin+params. • Add explicit unit tests for object key-order and Unicode normalization variants of the same logical payload.
🔵 STRIDE-4: Unbounded Ledger Growth Enabling Memory Exhaustion DoS
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 4.5 CVSS:4.0/AV:N/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: ConsentReplayLedger in background.ts allows resource exhaustion via repeated recordConsentRequired calls due to reliance on a configurable but potentially insufficiently-tuned maxEntries LRU-eviction limit, resulting in degraded extension performance or eviction of legitimate pending entries (self-inflicted DoS of the consent flow).
Evidence: packages/extension/tests/consent-replay.test.mts:97-108
test("tracked requests are bounded, oldest evicted first", () => {
const led = new ConsentReplayLedger({ maxEntries: 3 });
for (let i = 0; i < 5; i++) led.recordConsentRequired(replayKey(ORIGIN, { i }), `d${i}`);
assert.equal(led.size, 3);
});
Attack Scenario:
- A malicious or buggy page repeatedly calls handleRequestTask with unique
paramsvalues (e.g., incrementing a counter field) from the same origin, each producing a distinct replayKey. - Each call that receives a
consentRequiredresponse from the VTA causesconsentReplays.recordConsentRequired(key, digest)to insert a new ledger entry. - Test evidence confirms the ledger evicts oldest entries once
maxEntriesis reached ('tracked requests are bounded, oldest evicted first'), but the default production maxEntries value is unknown from the diff. - If maxEntries is large or unbounded in production configuration, sustained high-frequency requests consume growing memory in the long-lived background service worker until restart.
- If maxEntries is small, a flooding page can evict legitimate pending consentRequired entries for other origins/tabs, causing those users' valid one-time exemptions to silently fail and forcing extra prompts (functional DoS on the intended UX optimization) rather than a security bypass — but see STRIDE-1 for the inverse risk.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: handleRequestTask -> consentReplays.recordConsentRequired
Preconditions: Attacker page can trigger many distinct handleRequestTask calls in rapid succession., VTA responds with consentRequired for each attempt (or attacker can locally simulate volume before VTA round-trip).
Existing Controls: maxEntries option exists and defaults to eviction-safe behavior per test suite. • In-memory, per-service-worker-lifetime storage limits persistence of the exhaustion.
Recommended Mitigations: Set and document a conservative maxEntries value. • Apply per-origin rate limiting on handleRequestTask invocations independent of the ledger. • Emit telemetry/logging when eviction occurs to detect abuse patterns.
🟡 STRIDE-5: Missing Audit Log for Consent-Exempted Task Execution
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-778,CWE-223 |
| CAPEC | CAPEC-81 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: handleRequestTask in background.ts allows repudiation of exempted task submissions due to absence of logging when consumeIfArmed(key) bypasses the requestConsent prompt, resulting in insufficient forensic evidence to distinguish user-approved actions from silently-exempted replays.
Evidence: packages/extension/src/background.ts:1650-1656
if (!consentReplays.consumeIfArmed(key)) {
const approved = await requestConsent({...});
...
}
// no logging on the exemption path
Attack Scenario:
- A task request is submitted;
consentReplays.consumeIfArmed(key)returns true, so therequestConsent(...)branch (and any associated UI logging/telemetry tied to that prompt) is entirely skipped. - No code in the visible diff records that this specific submission bypassed the interactive consent dialog — the only observable artifact is the eventual chrome.runtime.sendMessage to OFFSCREEN_REQUEST_TASK, indistinguishable from a normally-approved flow.
- If a dispute arises later (e.g., 'I never approved this VTA task'), there is no local audit trail differentiating an exempted automatic replay from an explicit human click, weakening non-repudiation guarantees for security-sensitive actions.
- An attacker who successfully exploits STRIDE-1/STRIDE-2 to force an exemption gains additional cover, since the resulting task execution is not flagged in any log as consent-bypassed.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: handleRequestTask exemption branch -> OFFSCREEN_REQUEST_TASK dispatch
Preconditions: No dedicated audit logging exists for the exemption branch (inferred from absence in the diff; consent-replay.ts internals not reviewed).
Existing Controls: In-code comments document intended behavior, aiding future code review. • Test suite validates exemption logic correctness, indirectly supporting trust in when exemptions fire.
Recommended Mitigations: Emit a structured log entry (with timestamp, origin, params hash, and 'exempted' flag) whenever consumeIfArmed returns true and the interactive prompt is skipped. • Surface exemption usage in the extension's activity/history UI for user transparency. • Retain exemption audit events for a defined retention period to support incident investigation.
🔵 STRIDE-6: Information Disclosure of payloadDigest via Wallet Event Broadcast
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.8 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-200,CWE-359 |
| CAPEC | CAPEC-116 |
| OWASP | A01:2021 - Broken Access Control |
Description: broadcastWalletEvent in background.ts allows sensitive payloadDigest exposure to web pages due to fire-and-forget event broadcast of the same detail object used to arm the consent-replay ledger, resulting in disclosure of a security-relevant digest value to the requesting page or other listening pages.
Evidence: packages/extension/src/background.ts:2517-2530
if (m.event === "consentgranted") {
const digest = m.detail?.payloadDigest;
if (typeof digest === "string" && digest) consentReplays.recordGranted(digest);
}
void broadcastWalletEvent(m.event, m.detail);
Attack Scenario:
- Upon receiving
RUNTIME_EMIT_WALLET_EVENTwithevent: 'consentgranted', background.ts both arms the ledger viaconsentReplays.recordGranted(digest)and unconditionally callsvoid broadcastWalletEvent(m.event, m.detail), forwarding the samedetailobject (includingpayloadDigest) onward. - If
broadcastWalletEventrelays this to content scripts/pages via postMessage or a DOM CustomEvent (as implied by 'broadcast a wallet event to pages (e.g. consentgranted)'), any page listening for wallet events on that tab/origin receives the digest value. - An attacker-controlled page in the same tab context (e.g., via an iframe or a compromised sibling script) captures the digest.
- If digest values are reused or predictable across related requests, the attacker leverages the disclosed digest to attempt to prime or interfere with the exemption ledger for a different but related request, aiding STRIDE-1/STRIDE-2 exploitation chains.
🔎 Threat Clue: Derived from COMP-001 via EP-002
- Data Flows: RUNTIME_EMIT_WALLET_EVENT -> broadcastWalletEvent -> page listeners
Preconditions: broadcastWalletEvent forwards event detail broadly to page-reachable contexts., Digest value has some predictability or reuse across related task types.
Existing Controls: Digest is described as 'salted' by the VTA, reducing predictability. • Grant is single-use once consumed.
Recommended Mitigations: Strip or omit payloadDigest from the detail object forwarded to broadcastWalletEvent; only pass what pages legitimately need (e.g., event type, non-sensitive metadata). • Scope wallet event broadcasts strictly to the origin that initiated the corresponding request. • Rotate/increase entropy of digest values so disclosure has minimal exploitation value.
🔵 STRIDE-7: Type Confusion on Untrusted message Object Enabling Handler Logic Bypass
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/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 | A03:2021 - Injection |
Description: chrome.runtime.onMessage listener in background.ts allows unsafe type coercion via unchecked 'as' type assertions on the message parameter due to absence of runtime schema validation, resulting in potential logic errors or unintended code paths when a malformed or malicious message is delivered.
Evidence: packages/extension/src/background.ts:2517-2519
if ((message as { type?: string })?.type === RUNTIME_EMIT_WALLET_EVENT) {
const m = message as { event: WalletEventKind; detail?: Record<string, unknown> };
Attack Scenario:
- background.ts casts the incoming message with
(message as { type?: string })?.type === RUNTIME_EMIT_WALLET_EVENTand thenconst m = message as { event: WalletEventKind; detail?: Record<string, unknown> }— TypeScript'sasis compile-time only and performs no runtime validation. - An attacker crafts a message where
eventis not a validWalletEventKindstring but happens to equal the literal'consentgranted'string coincidentally, or wheredetailis an unexpected shape (e.g.,detail.payloadDigestis an object with atoStringthat returns a targeted digest, or a getter with side effects). - Because
typeof digest === 'string'is checked before use, primitive type confusion is partially mitigated, but any downstream code inconsent-replay.tsorbroadcastWalletEventthat trusts the declared TypeScript shape without further runtime checks (e.g., on otherdetailfields) may misbehave. - This weakens defense-in-depth: a single missing runtime validation on a security-relevant message type increases the risk that future code changes reintroduce exploitable assumptions.
🔎 Threat Clue: Derived from COMP-001 via EP-002
- Data Flows: chrome.runtime.onMessage -> message type assertion -> handler branch
Preconditions: Attacker or malicious extension component can deliver arbitrary-shaped objects to chrome.runtime.onMessage., Downstream consumers of m.detail beyond payloadDigest lack their own runtime validation (unverifiable from visible diff).
Existing Controls: Explicit typeof digest === 'string' && digest guard before use of payloadDigest. • Non-string/falsy digests are silently ignored rather than processed.
Recommended Mitigations: Introduce runtime schema validation (e.g., zod, io-ts) for all chrome.runtime.onMessage payloads before casting. • Enumerate and validate WalletEventKind values explicitly rather than relying on TypeScript type assertions. • Add fuzz testing for the onMessage listener with malformed/adversarial payload shapes.
🟡 STRIDE-8: Supply Chain Risk from Opaque Binary consent-replay.ts Diff Blob
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-1104,CWE-693 |
| CAPEC | CAPEC-437 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: The pull request diff allows unreviewable code introduction via a new file (consent-replay.ts) submitted as a binary diff due to the git diff representing it as 'Binary files /dev/null and b/... differ' rather than readable text, resulting in a security-critical authorization component that cannot be verified by human or automated code review before merge.
Evidence: packages/extension/src/consent-replay.ts:N/A
diff --git a/packages/extension/src/consent-replay.ts b/packages/extension/src/consent-replay.ts
new file mode 100644
index 0000000..141ffb0
Binary files /dev/null and b/packages/extension/src/consent-replay.ts differ
Attack Scenario:
- The diff for
packages/extension/src/consent-replay.tsis rendered asBinary files /dev/null and b/packages/extension/src/consent-replay.ts differ, meaning standard textual diff review (and potentially some CI diff-based security scanners) cannot inspect its contents line-by-line. - A malicious insider or compromised contributor account submits a PR where the visible background.ts changes look benign and well-commented (as seen here), while the actual security-critical logic hides in the unreviewable consent-replay.ts blob.
- Reviewers, relying on the reassuring comments and passing test suite, approve the PR without being able to diff the exact implementation change if this is a subsequent modification (for initial add, the whole file is simply invisible in diff form).
- If the actual implementation deviates from the tested behavior (e.g., contains a hidden backdoor origin allowlist, or a debug flag that always returns true from consumeIfArmed), it ships to production undetected because code review tooling flagged it as binary/non-diffable.
- This is a process/tooling weakness that compounds all threats above, since the actual authoritative implementation was never available for this analysis (per recon note: 'The core logic of ConsentReplayLedger... is not present in the provided source').
🔎 Threat Clue: Derived from COMP-001 via N/A
- Data Flows: N/A - build/review pipeline artifact
Preconditions: Source control / CI tooling treats the new file as binary (e.g., due to encoding artifacts, BOM, or git attribute misconfiguration)., Reviewers rely on test-passing and comments rather than requiring a readable diff before approval.
Existing Controls: Comprehensive test suite (consent-replay.test.mts) exercises many expected security properties. • Descriptive inline comments in background.ts explain intended security rationale.
Recommended Mitigations: Fix repository/.gitattributes configuration so .ts files are never treated as binary in diffs. • Require mandatory manual review of the full file content (not just the diff) for any new file touching authentication/consent/authorization logic. • Add a CI gate that fails the build if a diff for a security-sensitive path (e.g., matching consent, auth, ledger) is reported as binary. • Mandate signed commits and a second independent reviewer for changes to consent/authorization primitives.
🟡 STRIDE-9: Cross-Origin Ordering Attack Exploiting Grant-Before-Refusal Window
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.4 CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-362,CWE-841 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: The dual-event dependency between RUNTIME_EMIT_WALLET_EVENT and handleRequestTask in background.ts allows an ordering-manipulation attack due to the ledger's arming logic being dependent on message delivery order across two independent async channels, resulting in potential exemption grants for requests the VTA never actually reviewed in the intended sequence.
Evidence: packages/extension/tests/consent-replay.test.mts:48-55
test("a grant arriving before any refusal arms nothing", () => {
const led = new ConsentReplayLedger();
led.recordGranted(DIGEST);
led.recordConsentRequired(replayKey(ORIGIN, PARAMS), DIGEST);
assert.equal(led.consumeIfArmed(replayKey(ORIGIN, PARAMS)), false);
});
Attack Scenario:
- The system's correctness depends on
recordConsentRequiredalways being called (from the OFFSCREEN_REQUEST_TASK response handler) strictly before the correspondingrecordGranted(from the RUNTIME_EMIT_WALLET_EVENT listener), per the tested property 'a grant arriving before any refusal arms nothing'. - Both call sites are independent async message handlers in the same background.ts script but triggered by different message channels (chrome.runtime.sendMessage response vs. chrome.runtime.onMessage listener) that provide no strict ordering guarantee across all browser/service-worker scheduling conditions, especially under service-worker suspension/wake cycles.
- An attacker who can influence timing (e.g., by causing the offscreen document to be slow to respond, or by triggering service worker eviction between the two events) could attempt to cause the wallet event (consentgranted, from a legitimately-approved but unrelated flow) to be processed by the listener after a new consentRequired for a different request happens to be recorded under a colliding digest.
- While the current test suite explicitly defends against naive ordering issues within a single ledger instance, cross-request races under real browser scheduling (multiple simultaneous tabs/origins) are not demonstrated as tested, leaving residual uncertainty about atomicity guarantees under concurrent load.
🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-001, EP-002
- Data Flows: OFFSCREEN response -> recordConsentRequired, RUNTIME_EMIT_WALLET_EVENT -> recordGranted
Preconditions: High-precision timing control or multiple concurrent tabs from attacker-influenced origins., Service worker suspend/wake behavior creates non-deterministic message processing order (browser-implementation dependent).
Existing Controls: Explicit test: 'a grant arriving before any refusal arms nothing' — the ledger's own logic rejects this ordering for the SAME key. • Digest binding (not just origin/params) adds an additional matching requirement.
Recommended Mitigations: Use a single atomic message-passing mechanism or explicit sequence numbers to guarantee ordering between consentRequired and consentgranted processing. • Add integration/concurrency tests simulating multiple simultaneous origins and service-worker suspension to validate ordering guarantees hold under real Chrome scheduling. • Consider persisting critical state transitions (or at least sequence counters) to chrome.storage.session to survive worker restarts safely.
⚪ STRIDE-10: Prompt/Comment Injection Attempt Embedded in Reviewed Source Artifacts
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | Informational |
| Likelihood | Unlikely |
| CVSS | 0.0 CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-1039 |
| CAPEC | CAPEC-402 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: Source code comments and PR text in background.ts and consent-replay.test.mts allow analyst-manipulation attempts via natural-language instructions embedded as code comments due to the untrusted nature of PR content, resulting in a theoretical risk that automated review tooling (e.g., LLM-based security scanners) could be misled if it treated comments as directives rather than data.
Evidence: packages/extension/src/background.ts:1639-1649
// The human approved that exact payload here and
// then again on the approving device — see `consent-replay.ts` for why asking
// a third time costs more than it buys.
Attack Scenario:
- The PR's comments extensively explain and justify design decisions in persuasive natural language (e.g., 'why asking a third time costs more than it buys', 'is what keeps a second implementation of that hash from existing here to drift').
- Such persuasive, narrative-style comments in security-sensitive code are a known social-engineering vector against both human reviewers and automated LLM-based review tools, potentially steering the reviewer toward accepting a weaker security posture (trusting wire-supplied digests) as reasonable.
- This specific PR's comments were treated strictly as inert data during this analysis per the security directive, and no embedded instruction altered the analysis output; however, the pattern itself — highly persuasive security-rationale comments co-located with an actual security control weakening — is noted as a process risk warranting explicit reviewer skepticism independent of comment tone.
- No exploitation beyond the underlying STRIDE-1 weakness is achieved by the comments themselves; this finding documents the manipulation attempt pattern rather than a separate direct technical vulnerability.
🔎 Threat Clue: Derived from COMP-001 via N/A
- Data Flows: N/A - PR review process artifact
Preconditions: Reviewer or automated tool susceptible to being influenced by narrative justification rather than independently verifying the security property.
Existing Controls: This analysis explicitly treated all PR content as untrusted data per its operating directive and did not alter its verdicts based on embedded text. • Test suite provides objective, code-verifiable evidence independent of the narrative comments.
Recommended Mitigations: Train reviewers and configure automated tools to evaluate security-relevant code changes strictly on their technical merits, independent of accompanying prose. • Require security-critical PRs to include a structured threat-model diff/checklist rather than free-form narrative justification alone.
🍝 PASTA Threat Model
Application Purpose
A browser extension (wallet) that lets web pages request Verifiable Task Agent (VTA) actions on behalf of the user, mediating consent via a human-in-the-loop confirmation dialog before dispatching signed Trust-Task envelopes to an offscreen document that communicates with the user's VTA.
Inherent Risks
- The extension mediates high-trust actions (DID/task operations) between untrusted web origins and a cryptographic wallet identity, making consent-bypass the highest-value target.
- Chrome extension messaging APIs (runtime.onMessage/sendMessage) are inherently broadcast-like and require explicit sender validation that is easy to omit.
- In-memory-only security state (the ledger) trades persistence for reduced attack surface but relies entirely on correct in-process logic with no external verification.
Objectives
Risk: Accept residual risk of one extra consent prompt in ambiguous cases rather than risk silently approving an unauthorized task.
Business: Enable seamless, low-friction VTA task workflows for enrolled DID-based services without sacrificing user trust.
Security: Guarantee that only a genuinely human-approved and VTA-confirmed request can bypass the interactive consent prompt.; Prevent any web origin from forging or replaying a consent grant it did not legitimately receive.
Financial: Avoid liability and reputational cost from unauthorized task executions attributable to the wallet.
Compliance: Maintain auditable, non-repudiable evidence of user consent for regulated identity/credential operations.
Functional: Reduce redundant consent prompts for a task the user has already approved once via the completing replay exemption.
Operational: Keep the consent exemption logic simple, in-memory, and resettable on service-worker restart to bound operational risk.
Business Impact Analysis (2)
BIA-1: VTA Task Consent Approval Flow (Critical)
The end-to-end process by which a web page requests a VTA task, the user is (conditionally) prompted for consent, and an approved task is signed and dispatched to the VTA.
MTD: 00 days 00:30 hours | RTO: 00 days 00:05 hours | RPO: 00 days 00:00 hours
- Stakeholders: End Users / Extension Maintainers / VTA Operators / Web Page Integrators
- Dependencies: ConsentReplayLedger / Offscreen Document / VTA Backend Service / chrome.runtime Messaging APIs
- Disruptions: Consent bypass allowing unauthorized task execution / Service worker restart losing legitimate pending exemptions and forcing extra prompts / Malicious message spoofing arming exemptions without genuine approval
- Impacts: Unauthorized DID/task operations executed under the user's identity / Loss of user trust in the wallet's consent guarantees / Potential regulatory exposure if task pertains to verifiable credential operations
BIA-2: Wallet Event Broadcasting to Web Pages (Medium)
The process by which internal wallet state changes (e.g., consentgranted) are broadcast as DOM/runtime events to listening web pages.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: End Users / Web Page Integrators
- Dependencies: broadcastWalletEvent function / chrome.runtime Messaging APIs
- Disruptions: Sensitive digest values leaked to unintended page contexts / Malformed event payloads causing handler exceptions
- Impacts: Minor information disclosure aiding replay-ledger priming attacks / Degraded page integration reliability
Technical Scope
Roles (3): RO-1 Extension User · RO-2 Web Page Origin · RO-3 VTA Operator
Actors (3): AC-1 End User · AC-2 Web Page Script · AC-3 VTA Service
Entry Points (3): EP-001 handleRequestTask Message Handler · EP-002 RUNTIME_EMIT_WALLET_EVENT Listener · EP-003 OFFSCREEN_REQUEST_TASK Dispatch
Threat Actors (3): TA-1 Malicious Web Page Operator · TA-2 Compromised/Malicious Extension Component · TA-3 Malicious Insider Contributor
Infrastructure (1): IF-1 Browser Extension Runtime
Trust Boundaries (3): TB-1 Web Page to Extension Boundary · TB-2 Extension Internal Messaging Boundary · TB-3 Extension to VTA Backend Boundary
External Entities (2): EE-1 Requesting Web Page · EE-2 VTA Backend
System Components (5): SC-1 Background Service Worker · SC-2 ConsentReplayLedger · SC-3 Offscreen Document · SC-4 Web Page (Requesting Origin) · SC-5 VTA Backend Service
Resources And Assets (3): RA-1 Consent Exemption Ledger State · RA-2 payloadDigest Value · RA-3 Trust-Task Envelope Signature Key (did:peer #key-2)
Technologies And Dependencies (3): TD-1 Chrome Extension Manifest V3 APIs · TD-2 ConsentReplayLedger Module · TD-3 Node.js Test Runner
Use Cases (2)
- VTA Task Request with User Consent: A web page requests a VTA task; the extension prompts the user for consent, and upon approval signs and dispatches the task to the VTA via the offscreen document.
- Consent Replay Exemption Completion: After an initial consent refusal is recorded and a matching grant is relayed from the VTA, a same-payload resubmission from the same origin completes the ceremony without a second interactive prompt.
📋 Risk Registry (7)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Consent bypass allowing unauthorized VTA task execution via forged/replayed digest trust. | High | Medium | Immediate | Medium |
| RISK-002 | Unreviewable binary diff for a new security-critical authorization file bypassing code review scrutiny. | Medium | Medium | Immediate | Low |
| RISK-003 | Timing and ordering races in the two-channel consent ledger update sequence. | Medium | Low | Short-Term | Medium |
| RISK-004 | Lack of audit trail distinguishing consent-exempted task executions from interactively approved ones. | Medium | Low | Short-Term | Low |
| RISK-005 | Unbounded or misconfigured ledger growth leading to memory exhaustion or unintended eviction of legitimate exemptions. | Low | Low | Medium-Term | Low |
| RISK-006 | Sensitive digest values disclosed to web pages via wallet event broadcast, aiding chained exploitation. | Low | Low | Medium-Term | Low |
| RISK-007 | Absence of runtime schema validation on internal extension messages enabling type confusion. | Low | Low | Long-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
CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
CWE346@{ shape: rect, label: "CWE-346: Origin Validation Error" }
CWE1104@{ shape: rect, label: "CWE-1104: Use of Unmaintained Third Party Components" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC60@{ shape: rect, label: "CAPEC-60: Reusing Session IDs (aka Session Replay)" }
CAPEC148@{ shape: rect, label: "CAPEC-148: Content Spoofing" }
CAPEC437@{ shape: rect, label: "CAPEC-437: Supply Chain" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE1@{ shape: rect, label: "STRIDE-1: Consent Bypass via Digest Trust<br><i>High / Likely</i>" }
STRIDE2@{ shape: rect, label: "STRIDE-2: Sender Spoofing of Wallet Event<br><i>High / Likely</i>" }
STRIDE8@{ shape: rect, label: "STRIDE-8: Opaque Binary Diff Blob<br><i>Medium / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Web Page Operator<br><i>Bypass consent for unauthorized tasks</i>" }
TA2@{ shape: rect, label: "TA-2: Compromised Extension Component<br><i>Spoof internal wallet events</i>" }
TA3@{ shape: rect, label: "TA-3: Malicious Insider Contributor<br><i>Hide backdoor in unreviewable diff</i>" }
end
CWE345 --> CAPEC60 --> STRIDE1 --> TA1
CWE346 --> CAPEC148 --> STRIDE2 --> TA2
CWE1104 --> CAPEC437 --> STRIDE8 --> TA3
SC1 --> CWE345
SC1 --> CWE346
SC1 --> CWE1104
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
SC-2: ConsentReplayLedger
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC2@{ shape: rect, label: "SC-2: ConsentReplayLedger" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE367@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
CWE770@{ shape: rect, label: "CWE-770: Allocation of Resources Without Limits" }
CWE362@{ shape: rect, label: "CWE-362: Concurrent Execution using Shared Resource" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC26@{ shape: rect, label: "CAPEC-26: Leveraging Race Conditions" }
CAPEC125@{ shape: rect, label: "CAPEC-125: Flooding" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE3@{ shape: rect, label: "STRIDE-3: TOCTOU Race in Ledger Arming<br><i>Medium / Possible</i>" }
STRIDE4@{ shape: rect, label: "STRIDE-4: Ledger Growth Memory Exhaustion<br><i>Low / Possible</i>" }
STRIDE9@{ shape: rect, label: "STRIDE-9: Cross-Origin Ordering Attack<br><i>Medium / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Web Page Operator<br><i>Bypass consent for unauthorized tasks</i>" }
end
CWE367 --> CAPEC26 --> STRIDE3 --> TA1
CWE362 --> CAPEC26 --> STRIDE9 --> TA1
CWE770 --> CAPEC125 --> STRIDE4 --> TA1
SC2 --> CWE367
SC2 --> CWE770
SC2 --> CWE362
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#FFA500,stroke-width:2px
linkStyle 2 stroke:#00FF00,stroke-width:2px
linkStyle 3 stroke:#FF0000,stroke-width:2px
linkStyle 4 stroke:#FF0000,stroke-width:2px
linkStyle 5 stroke:#FF0000,stroke-width:2px
SC-3: Offscreen Document
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC3@{ shape: rect, label: "SC-3: Offscreen Document" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE200@{ shape: rect, label: "CWE-200: Exposure of Sensitive Information" }
CWE843@{ shape: rect, label: "CWE-843: Type Confusion" }
CWE778@{ shape: rect, label: "CWE-778: Insufficient Logging" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC116@{ shape: rect, label: "CAPEC-116: Excavation" }
CAPEC153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
CAPEC81@{ shape: rect, label: "CAPEC-81: Web Server Logs Tampering" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE6@{ shape: rect, label: "STRIDE-6: Digest Disclosure via Broadcast<br><i>Low / Possible</i>" }
STRIDE7@{ shape: rect, label: "STRIDE-7: Type Confusion on Message Object<br><i>Low / Possible</i>" }
STRIDE5@{ shape: rect, label: "STRIDE-5: Missing Audit Log for Exemption<br><i>Medium / Likely</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Web Page Operator<br><i>Bypass consent for unauthorized tasks</i>" }
TA2@{ shape: rect, label: "TA-2: Compromised Extension Component<br><i>Spoof internal wallet events</i>" }
end
CWE200 --> CAPEC116 --> STRIDE6 --> TA1
CWE843 --> CAPEC153 --> STRIDE7 --> TA2
CWE778 --> CAPEC81 --> STRIDE5 --> TA1
SC3 --> CWE200
SC3 --> CWE843
SC3 --> CWE778
linkStyle 0 stroke:#00FF00,stroke-width:2px
linkStyle 1 stroke:#00FF00,stroke-width:2px
linkStyle 2 stroke:#FFA500,stroke-width:2px
linkStyle 3 stroke:#00FF00,stroke-width:2px
linkStyle 4 stroke:#00FF00,stroke-width:2px
linkStyle 5 stroke:#FFA500,stroke-width:2px
📊 Risk Summary
Total Threats: 10
By Severity: Low: 3 · High: 2 · Medium: 4 · Informational: 1
By Category: Spoofing: 3 · Tampering: 7 · Elevation of Privilege: 5 · Denial of Service: 1 · Repudiation: 1 · Information Disclosure: 1
🎯 Attack Surface
Kill Chain 1: An attacker-controlled web page (TA-1) exploits the unauthenticated RUNTIME_EMIT_WALLET_EVENT listener (STRIDE-2, EP-002) to inject a forged consentgranted event carrying an attacker-chosen or leaked payloadDigest, which the background service worker (SC-1) accepts without sender verification and records via consentReplays.recordGranted; combined with the wire-trusted digest recorded earlier from a consentRequired response (STRIDE-1), this arms the ConsentReplayLedger (SC-2) exemption for a specific origin+params key, allowing the attacker's page to resubmit its task request through handleRequestTask (EP-001) and have consumeIfArmed bypass the mandatory requestConsent prompt entirely, resulting in an unauthorized signed Trust-Task envelope being dispatched to the VTA (SC-5) under the victim's holder key — a full authentication-to-authorization bypass chain requiring no user interaction beyond the page load itself. Kill Chain 2: A malicious insider contributor (TA-3) leverages the repository's binary-diff rendering of the newly introduced consent-replay.ts (STRIDE-8) to smuggle a subtly weakened digest-verification or key-normalization implementation past code review, which — even though the visible test suite in consent-replay.test.mts passes — could contain edge-case behavior (e.g., inconsistent key canonicalization feeding STRIDE-3's TOCTOU race, or ordering assumptions violated under service-worker suspension per STRIDE-9) that silently widens the 'one exempt replay' into a broader bypass window, compounding Kill Chain 1's impact without any additional attacker action once merged. Kill Chain 3: Independent of active exploitation, the missing audit logging on the exemption path (STRIDE-5) combined with the digest disclosure via broadcastWalletEvent (STRIDE-6) means that even a successful Kill Chain 1 or 2 attack leaves minimal forensic trace — the disclosed digest could be reused by a colluding second page context to prime further exemptions, while the absence of exemption-specific logging prevents defenders from distinguishing the resulting unauthorized task dispatch from a normal, user-approved one during incident response.
🛡️ Risk Mitigation Strategy
Priority 1 (Immediate): Close the core trust gap that allows consent bypass — require the background service worker to independently verify or cryptographically validate the payloadDigest rather than trusting values sourced directly from either the offscreen document's response or the RUNTIME_EMIT_WALLET_EVENT message, and enforce sender identity checks (sender.id equality, offscreen document URL matching) on all internal wallet-event messages before they can arm the ConsentReplayLedger; this directly addresses RISK-001 (STRIDE-1, STRIDE-2), the highest-severity and most likely-to-be-exploited path in this diff. Priority 2 (Immediate): Fix the repository/CI tooling so that new files under security-sensitive paths (consent, auth, ledger) are never rendered or accepted as binary diffs, and mandate a full-file manual review plus a second independent approver for any PR touching consent or authorization primitives, closing the process gap identified in RISK-002 (STRIDE-8) that currently allows an entire security-critical module to be merged without line-level review. Priority 3 (Short-Term): Add structured audit logging for every consumed consent-replay exemption (origin, params hash, timestamp, digest reference) and surface this in a user-facing activity/history view, addressing the non-repudiation gap in RISK-004 (STRIDE-5); in parallel, harden key canonicalization in replayKey and add concurrency/ordering integration tests across simulated multi-tab and service-worker-suspension scenarios to reduce the TOCTOU and ordering risks captured in RISK-003 (STRIDE-3, STRIDE-9). Priority 4 (Medium-to-Long-Term): Reduce residual attack surface by stripping sensitive digest values from page-facing wallet event broadcasts (RISK-006), tuning and documenting ledger maxEntries with per-origin rate limiting to prevent both exhaustion and eviction-based denial of the legitimate exemption UX (RISK-005), and introducing runtime schema validation (e.g., zod/io-ts) for all chrome.runtime
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 3 | 0 |
Confirmed (3)
- 🟡 Consent-replay ledger trusts wire-supplied payloadDigest without independent verification
- 🟡 Consent replay exemption keyed only on digest allows cross-origin/cross-request confusion if digest collides or is attacker-influenced
- 🟡 Missing audit logging on consent-exemption (bypass) code path weakens repudiation defenses (triaged LOW→MEDIUM)
A consent-gated DID edit prompted the operator three times: the worker-mode
confirm, the approval ceremony on their device, and then the same
worker-mode confirm again when the page replayed its pinned re-submit.
The third prompt asks nothing new. By the time it fires the human has
approved that exact payload twice — once here, once on the approving
device, which showed the VTA's dry-run effects and a match code to compare
— and the VTA is holding a single-use grant bound to the payload's digest.
It is the same question, asked again on the weaker of the two surfaces.
That is not free. The VTA's own
policy_gate.rsstates the cost: consentdesigns die to habituation long before they die to cryptography. Three
prompts an edit, one of them uninformative, is how a person is trained to
click through the one that matters.
The gate itself is unchanged and still un-skippable — its reason holds, and
is unrelated: with
policy.enforcementoff it is the only thing between anarbitrary page and an arbitrary task. What changes is that one replay is
recognised as the completion of a ceremony rather than a new request.
The exemption requires all three of: the same origin and byte-identical
params; a prior
consentRequiredrefusal for them, so only a task that didnot run is ever tracked; and a
task-consent/grantedsince relayed,matched on the VTA's own
payloadDigest. It is single-use and expires withthe grant it depends on (600 s, the VTA's
GRANT_TTL_SECS).The digest is never recomputed here. It comes off the wire twice — from the
VTA's refusal and from its granted notice — so this adds no second
implementation of a consensus-critical hash to drift out of step. That
mattered: the digest construction is domain-tagged, length-prefixed and
JCS-canonical, and a divergent copy would fail as a mismatch nobody sees.
Every failure mode falls back to prompting. A key that does not match, a
grant that never arrives, a page that re-serialises its payload between
submits, a service-worker restart that empties the in-memory ledger — all
show the confirm again, which is the direction a mistake should fail in.
The state machine is a pure module so it can be tested without
chrome,following
sites-model.ts. Eleven tests, and most of them are about whatmust not be exempt: another payload from the same origin, the same
payload from another origin, a grant for an unrelated digest, a grant that
arrives before any refusal, a re-refused entry, and anything past the TTL.
Signed-off-by: Glenn Gore glenn.g@affinidi.com