Skip to content

fix: stop asking a third time to publish a change already approved twice - #154

Merged
stormer78 merged 1 commit into
mainfrom
fix/consent-replay-exemption
Sep 1, 2026
Merged

fix: stop asking a third time to publish a change already approved twice#154
stormer78 merged 1 commit into
mainfrom
fix/consent-replay-exemption

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

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

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>
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Aug 31, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

3 AI-confirmed issues.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #154

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/consent-replay-exemptionmain
Validated 2026-09-05
Scan ID 5970db53
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 2 · findings: 4

Module Files scanned Findings
packages/extension 2 4

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 3 0

🔒 Security Issues

Confirmed Vulnerabilities (3)

🟡 Consent-replay ledger trusts wire-supplied payloadDigest without independent verification

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:1666
Finding ID github_pr-c6fde4328556
CWE CWE-345, CWE-290
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1556 - Modify Authentication Process
CAPEC CAPEC-60
DREAD 5.2
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE-345/807 trust issue with code-evidence-confirmed reachability (background.ts:462-463) but exploitation requires a compound precondition — prior local compromise granting IndexedDB write access — which is not attacker-controlled remotely and has no network exposure. Impact is bounded to badge/UI spoofing as a social-engineering pretext, not direct authorization bypass of the DIDComm approval flow itself. This matches medium calibration: CVSS N/A but effectively CVSS 4-6.9 equivalent impact, no confirmed real-world exploit, specific conditions (local compromise) required for exploitation.
  • Composite score: 4.8
  • Environment: unknown

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

await ensureOffscreenDocument();
const res = (await chrome.runtime.sendMessage({
  target: OFFSCREEN_TARGET,
  type: OFFSCREEN_REQUEST_TASK,
  vtaDid: active.conn.vtaDid,
  origin: req.origin,
  params: req.params,
})) as RuntimeRequestTaskResponse;

if (res.ok && res.result?.kind === "consentRequired") {
  const digest = res.result.payloadDigest;
  if (typeof digest === "string" && digest) consentReplays.recordConsentRequired(key, digest);
}
return res;

Vulnerable lines: 1660, 1675

🔁 Reproduction Steps:

  1. Compromise or MITM the offscreen document <-> VTA communication path (or a bug that lets a page influence the OFFSCREEN_REQUEST_TASK response).
  2. Return a consentRequired response with an attacker-chosen payloadDigest string instead of the value the VTA would legitimately compute for the actual payload.
  3. Observe consentReplays.recordConsentRequired(key, digest) stores this attacker-chosen digest as the value that will later be matched against a 'granted' event.
  4. Trigger (or await) a consentgranted event carrying the same attacker-chosen digest (see VULN-001) to arm the exemption for a request whose digest was never actually validated against its content.

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

if (res.ok && res.result?.kind === "consentRequired") {
  const digest = res.result.payloadDigest;
  if (typeof digest === "string" && digest) consentReplays.recordConsentRequired(key, digest);
}
return res;

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

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001 handleRequestTask -> chrome.runtime.sendMessage to OFFSCREEN_TARGET/OFFSCREEN_REQUEST_TASK -> response.result.kind==='consentRequired' -> res.result.payloadDigest taken verbatim -> consentReplays.recordConsentRequired(key, digest) at background.ts ~1671-1673

⚖️ Triage Factors:

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

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:

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

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:

if (res.ok && res.result?.kind === "consentRequired") {
  const digest = res.result.payloadDigest;
  if (typeof digest === "string" && digest) consentReplays.recordConsentRequired(key, digest);
}

Secure code:

if (res.ok && res.result?.kind === "consentRequired") {
  const digest = res.result.payloadDigest;
  const expected = await computeCanonicalPayloadDigest(req.origin, req.params); // locally derived, same algorithm as VTA
  if (typeof digest === "string" && digest && digest === expected) {
    consentReplays.recordConsentRequired(key, digest);
  } else if (typeof digest === "string" && digest) {
    console.warn("payloadDigest mismatch — refusing to arm replay ledger", { origin: req.origin });
  }
}

Additional recommendations:

  • Require the VTA's grant notice to include a signature over the payloadDigest verifiable against the VTA's known DID/key.
  • Add integrity checks (e.g., message authentication) on the offscreen-document-to-background channel.
  • Add regression tests asserting that a digest not matching the actual payload never arms the ledger.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: In background.ts (diff hunk, handleRequestTask): if (res.ok && res.result?.kind === "consentRequired") { const digest = res.result.payloadDigest; if (typeof digest === "string" && digest) consentReplays.recordConsentRequired(key, digest); } — the comment explicitly states 'Taking it from the wire rather than recomputing it is what keeps a second implementation of that hash from existing here to drift.' This confirms the digest is trusted verbatim from the `chrome.runtime.sendMe
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Consent replay exemption keyed only on digest allows cross-origin/cross-request confusion if digest collides or is attacker-influenced

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:1652
Finding ID github_pr-3a70fc94cb9e
CWE CWE-346
OWASP A01:2021-Broken Access Control
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE-346 origin validation weakness confirmed in code (digest-only replay key with no nonce/freshness binding), but exploitation requires a compounding precondition (message injection or compromised offscreen document) not independently demonstrated. Scanner confidence is only 35% and reachability is 'no-info'. This is a real design flaw worth fixing but does not meet high/critical criteria absent a standalone exploitation path.
  • Composite score: 4.9
  • Environment: production

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

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:

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.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 35%
  • AI Validation Evidence: EVIDENCE FOUND: handleRequestTask in background.ts: 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" }; } — confirms that when consumeIfArmed returns true, the interactive requestConsent prompt is entirely skipped, exactly as described. The exemption keys strictly on origin + JSON.stringify(params) (see consent-replay.ts `repl
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Missing audit logging on consent-exemption (bypass) code path weakens repudiation defenses

Field Detail
Severity MEDIUM
Location packages/extension/src/background.ts:1650
Finding ID github_pr-867666307666
CWE CWE-778, CWE-223
OWASP A09:2021 - Security Logging and Monitoring Failures
MITRE ATT&CK T1562.001 - Impair Defenses: Disable or Modify Tools
CAPEC CAPEC-81
DREAD 4.8
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — The flaw is a logging/repudiation gap (CWE-778-class), not a direct authorization bypass or injection — the replay mechanism still requires a prior legitimate consumeIfArmed() token to be armed, meaning no new privilege or data access is granted. Exploit maturity is theoretical, there is no CVE/EPSS/KEV evidence, and impact is confined to audit-trail completeness rather than CIA impact. This does not meet medium calibration thresholds (CVSS 4-6.9 equivalent impact) let alone high/critical.
  • Composite score: 5
  • Environment: production

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.

const key = replayKey(req.origin, req.params);
if (!consentReplays.consumeIfArmed(key)) {
  const approved = await requestConsent({
    origin: req.origin,
    action: `send a "${taskLabel(req.params.type)}" request to your VTA`,
    noRemember: true,
  });
  if (!approved.approved) return { ok: false, error: "user denied the request" };
}

await ensureOffscreenDocument();

Vulnerable lines: 1645, 1660

🔁 Reproduction Steps:

  1. Successfully arm a replay exemption (e.g., via the legitimate happy path, or via VULN-001's spoofing chain).
  2. Resubmit the identical task request so that consumeIfArmed(key) returns true.
  3. Inspect extension logs/console/telemetry — observe no record differentiating this execution from a normally user-approved one.

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

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" };
}

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

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001 handleRequestTask -> consumeIfArmed(key)===true branch -> requestConsent() skipped -> OFFSCREEN_REQUEST_TASK dispatched with no distinguishing audit log entry

🔧 Remediation:

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

Vulnerable code:

const key = replayKey(req.origin, req.params);
if (!consentReplays.consumeIfArmed(key)) {
  const approved = await requestConsent({ origin: req.origin, action: `send a "${taskLabel(req.params.type)}" request to your VTA`, noRemember: true });
  if (!approved.approved) return { ok: false, error: "user denied the request" };
}

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: In handleRequestTask (background.ts diff): const key = replayKey(req.origin, req.params); if (!consentReplays.consumeIfArmed(key)) { const approved = await requestConsent({...}); if (!approved.approved) return {...}; } followed directly by await ensureOffscreenDocument(); and the chrome.runtime.sendMessage dispatch — there is no console.log, telemetry call, or any logging statement on the branch where consumeIfArmed(key) returns true (i.e., the exemption path). Search
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.


Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.

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

🛡️ Threat Model & Affect Analysis — PR #154

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/consent-replay-exemptionmain
Generated 2026-09-05

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

This PR 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 consentRequired and a matching grant (correlated by a VTA-supplied payload digest) has since arrived, the interactive consent prompt is skipped exactly once. The stated intent is to stop asking users a third time for a change they already approved twice (once locally, once on the approving device). The core exemption logic lives in a new file, consent-replay.ts, which is present in the diff only as an unreadable binary blob — its actual implementation could not be reviewed.

Diff: +168 / -8 lines
Types: security, feature, test

⚠️ Security Implications

🟠 Mandatory consent prompt replaced with a conditionally-bypassable one, backed by an unreviewed implementation

Mandatory consent prompt replaced with a conditionally-bypassable one, backed by an unreviewed implementation

Action: Require full source review of consent-replay.ts before merge; treat this as the highest-priority manual review item.

🟠 RUNTIME_EMIT_WALLET_EVENT listener arms the exemption ledger without sender verification

RUNTIME_EMIT_WALLET_EVENT listener arms the exemption ledger without sender verification

Action: Validate sender.id === chrome.runtime.id and restrict acceptance of this event to the extension's own offscreen document context; consider a dedicated private port channel instead of the shared onMessage bus for this trust signal.

🟠 payloadDigest trusted verbatim from the wire instead of being recomputed or cryptographically verified

payloadDigest trusted verbatim from the wire instead of being recomputed or cryptographically verified

Action: Bind the digest cryptographically to the specific consent ceremony (e.g., HMAC keyed to a value only the legitimate VTA/offscreen path knows, or a signature verifiable against the enrolled VTA's public key).

🧩 Affected Components

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:

  1. Attacker-controlled or compromised VTA (or a MITM on the offscreen->VTA channel) crafts a consentRequired response with an attacker-chosen payloadDigest string, sent back through chrome.runtime.sendMessage to handleRequestTask in background.ts.
  2. handleRequestTask calls consentReplays.recordConsentRequired(key, digest) using the attacker-influenced digest verbatim, per the code comment 'Taking it from the wire rather than recomputing it'.
  3. The same or colluding party later triggers RUNTIME_EMIT_WALLET_EVENT with event: 'consentgranted' and detail.payloadDigest matching the previously recorded digest — this handler in the chrome.runtime.onMessage.addListener block calls consentReplays.recordGranted(digest) without verifying the grant genuinely originated from the enrolled VTA's authenticated channel path beyond the offscreen 'inbound path' assumption.
  4. Because key = replayKey(req.origin, req.params) only binds origin+params (not the digest's provenance or cryptographic authenticity), any page at req.origin that resubmits the identical params now has consumeIfArmed(key) return true.
  5. handleRequestTask skips requestConsent(...) entirely and proceeds to ensureOffscreenDocument() and dispatch of OFFSCREEN_REQUEST_TASK, executing the VTA task with no human-in-the-loop confirmation.
  6. 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:

  1. 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 validate sender.id, sender.url, or sender.origin.
  2. Per EP-002 recon (auth_required: false), any extension component or, depending on manifest externally_connectable configuration, potentially an external page can dispatch a message of this type.
  3. Attacker crafts { type: RUNTIME_EMIT_WALLET_EVENT, event: 'consentgranted', detail: { payloadDigest: '<guessed-or-leaked-digest>' } } and sends it via chrome.runtime.sendMessage from any context able to reach the extension's message bus.
  4. 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.
  5. 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:

  1. Page A submits a task request at time T0; handleRequestTask records consentRequired with digest D via consentReplays.recordConsentRequired(key, D).
  2. 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.
  3. If replayKey normalization is imperfect (untestable here since consent-replay.ts implementation is not in scope), two logically different requests could map to the same ledger key.
  4. When the legitimate grant callback fires and calls recordGranted(D), both the original and the race-injected request become eligible for consumeIfArmed.
  5. 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:

  1. A malicious or buggy page repeatedly calls handleRequestTask with unique params values (e.g., incrementing a counter field) from the same origin, each producing a distinct replayKey.
  2. Each call that receives a consentRequired response from the VTA causes consentReplays.recordConsentRequired(key, digest) to insert a new ledger entry.
  3. Test evidence confirms the ledger evicts oldest entries once maxEntries is reached ('tracked requests are bounded, oldest evicted first'), but the default production maxEntries value is unknown from the diff.
  4. 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.
  5. 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:

  1. A task request is submitted; consentReplays.consumeIfArmed(key) returns true, so the requestConsent(...) branch (and any associated UI logging/telemetry tied to that prompt) is entirely skipped.
  2. 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.
  3. 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.
  4. 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:

  1. Upon receiving RUNTIME_EMIT_WALLET_EVENT with event: 'consentgranted', background.ts both arms the ledger via consentReplays.recordGranted(digest) and unconditionally calls void broadcastWalletEvent(m.event, m.detail), forwarding the same detail object (including payloadDigest) onward.
  2. If broadcastWalletEvent relays 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.
  3. An attacker-controlled page in the same tab context (e.g., via an iframe or a compromised sibling script) captures the digest.
  4. 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:

  1. background.ts casts the incoming message with (message as { type?: string })?.type === RUNTIME_EMIT_WALLET_EVENT and then const m = message as { event: WalletEventKind; detail?: Record<string, unknown> } — TypeScript's as is compile-time only and performs no runtime validation.
  2. An attacker crafts a message where event is not a valid WalletEventKind string but happens to equal the literal 'consentgranted' string coincidentally, or where detail is an unexpected shape (e.g., detail.payloadDigest is an object with a toString that returns a targeted digest, or a getter with side effects).
  3. Because typeof digest === 'string' is checked before use, primitive type confusion is partially mitigated, but any downstream code in consent-replay.ts or broadcastWalletEvent that trusts the declared TypeScript shape without further runtime checks (e.g., on other detail fields) may misbehave.
  4. 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:

  1. The diff for packages/extension/src/consent-replay.ts is rendered as Binary 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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:

  1. The system's correctness depends on recordConsentRequired always being called (from the OFFSCREEN_REQUEST_TASK response handler) strictly before the corresponding recordGranted (from the RUNTIME_EMIT_WALLET_EVENT listener), per the tested property 'a grant arriving before any refusal arms nothing'.
  2. 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.
  3. 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.
  4. 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:

  1. 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').
  2. 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.
  3. 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.
  4. 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
Loading

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
Loading

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
Loading

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

@stormer78
stormer78 merged commit 5a73e80 into main Sep 1, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/consent-replay-exemption branch September 1, 2026 07:05
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