chore(vtc-admin): drop the walletProfile capability probe - #1214
Conversation
`isWalletProfileAvailable()` existed so #1212 could merge before the wallet method it depends on did. That has now shipped (OpenVTC/vta-browser-plugin#145), so the second probe describes a wallet build that does not exist: every extension that exposes `proxyLogin` exposes `walletProfile` too. Folded into `isWalletProxyAvailable()`, which now checks the three methods this page actually calls. The proxy button is no longer conditionally hidden, and the secondary button no longer changes its label to stand in as the primary route — both were arms for a wallet nobody has. Presence detection stays and is not the same thing: the extension may simply not be installed, which is why the buttons are gated at all. Nothing about the flow changes. `walletProfile` → `/auth/challenge` → `proxyLogin` is untouched, and "Sign in as a different identity…" keeps the entry picker for an operator holding more than one persona here. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review2 AI-confirmed issues, 1 finding needs a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #1214
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 2 · findings: 4
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (2)🟡 Broken Access Control: Merged capability check exposes vault-enumeration UI to broader wallet states
🧠 AI Triage:
Summary: A capability-tier merge in wallet.ts removed the distinct isWalletProfileAvailable() check, causing Login.tsx to unconditionally surface the vault-enumerating 'choose identity' button whenever only the looser isWalletProxyAvailable() check passes. This broadens the conditions under which the full DID vault can be disclosed to the page via a single consent click, per the removed code's own documentation of the risk. 📝 Description: An attacker-influenced or degraded wallet extension build can cause the admin-ui to present the full vault-enumeration flow (vaultList) even in states where the wallet cannot reliably self-resolve an identity, increasing occasions where the complete list of DID identities bound in a user's wallet is disclosed to the web page context. 🧪 Proof of Concept: Both buttons are gated only by proxyAvailable (isWalletProxyAvailable), which was previously a coarser check than isWalletProfileAvailable used to gate the vault-enumeration button. The merge removes the finer-grained distinction the code's own comments describe as important, exposing a vault-enumerating consent flow to a wider range of wallet capability states. Vulnerable lines: 265, 296 🔎 Evidence: 💥 Impact: An attacker-influenced or degraded wallet extension build can cause the admin-ui to present the full vault-enumeration flow (vaultList) even in states where the wallet cannot reliably self-resolve an identity, increasing occasions where the complete list of DID identities bound in a user's wallet is disclosed to the web page context. 🧭 Reachability:
⚖️ Triage Factors:
Attack scenario: A degraded/malicious wallet extension satisfying only the looser proxy-availability check causes the admin-ui to expose the vault-enumerating 'choose identity' UI more broadly than intended, disclosing DID identities. 🔧 Remediation:
Reinstating the two-tier check restores the original intent: the vault-enumerating 'choose identity' path should only be exposed when the wallet build is verified to have full identity-resolution semantics, not merely to expose the raw functions. Vulnerable code: Secure code:
🟡 Unhandled TypeError via non-null assertion after removal of walletProfile-specific presence check
🧠 AI Triage:
Summary: The specific walletProfile-presence check was removed from loginWithWalletProfile(); the function now relies on isWalletProxyAvailable() and a non-null assertion, meaning a wallet build lacking walletProfile causes an uncaught TypeError rather than a graceful, informative error, potentially leaving the UI in a stuck 'busy' state. 📝 Description: Users on wallet builds lacking walletProfile experience an ungraceful crash of the proxy-login flow, with no actionable error message, degrading login availability and creating a poor/confusing failure mode that could mask a security-relevant capability mismatch. 🧪 Proof of Concept: The Vulnerable lines: 238, 250 🔎 Evidence: 💥 Impact: Users on wallet builds lacking walletProfile experience an ungraceful crash of the proxy-login flow, with no actionable error message, degrading login availability and creating a poor/confusing failure mode that could mask a security-relevant capability mismatch. 🧭 Reachability:
⚖️ Triage Factors:
Attack scenario: A wallet extension lacking walletProfile but exposing proxyLogin/vaultList causes an uncaught TypeError in loginWithWalletProfile, crashing the login flow instead of showing a graceful error. 🔧 Remediation:
Explicitly checking for walletProfile's presence before calling it, and removing the non-null assertion, restores a graceful, descriptive error path and avoids relying on TypeScript-only type assertions for runtime safety. Vulnerable code: Secure code:
|
| Field | Detail |
|---|---|
| Severity | HIGH |
| Location | vtc-service/admin-ui/src/lib/wallet.ts:32 |
| Finding ID | github_pr-f762fc4fef8d |
| CWE | CWE-352 |
| OWASP | A01:2021 - Broken Access Control |
| MITRE ATT&CK | T1556 - Modify Authentication Process |
| CAPEC | CAPEC-62 |
| Reachability | ⚪ Not reachable |
| Exploit Maturity | theoretical |
| Detection Source | skill_scan |
🔎 Evidence: vtc-service/admin-ui/src/lib/wallet.ts:32
/** API base for the wallet's auth round-trip. Points at the VTC's
* header-exempt wallet surface, served same-origin with the admin UI. */
function walletApiBase(): string {
🧭 Reachability:
- Network exposure: internal
- Auth barrier: unknown
- Attack path: Cannot be fully confirmed: the actual server-side endpoint implementing walletApiBase()'s target is not present in the provided source (only the client-side base-URL function was supplied). Attack path is inferred from documentation comment only.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND: wallet.ts comment states
// The wallet posts to ${baseUrl}/auth/challenge and ${baseUrl}/auth/ with no Trust-Task header, so we point baseUrl at the VTC's header-exempt /v1/wallet surface.and walletApiBase() returns${origin}/v1/wallet. The router.rs TrustTaskRouter explicitly documents route_exempt as intended 'only for /health' — '/v1/wallet' is not mentioned there, so exemption mechanics for /v1/wallet endpoints are not shown in provided files. EVIDENCE NOT FOUND: The act- Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.
Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.
Details
🛡️ Threat Model & Affect Analysis — PR #1214
| Field | Value |
|---|---|
| Repository | OpenVTC/verifiable-trust-infrastructure |
| Branch | chore/drop-wallet-profile-probe → main |
| Generated | 2026-09-05 |
ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.
📋 Affect Analysis
Change Summary
This PR consolidates two previously distinct wallet-capability detection tiers (isWalletProxyAvailable and isWalletProfileAvailable) into a single check in the admin-ui login flow, on the stated rationale that any wallet extension build exposing proxyLogin/vaultList always also exposes walletProfile. It simplifies Login.tsx by removing the conditional button/text logic that differentiated wallet builds with vs without walletProfile support, and narrows the loginWithWalletProfile() error message.
Diff: +34 / -41 lines
Types: refactor, security
⚠️ Security Implications
🟡 Removal of isWalletProfileAvailable() collapses a documented capability-tier distinction, broadening vault-enumeration UI exposure
Removal of isWalletProfileAvailable() collapses a documented capability-tier distinction, broadening vault-enumeration UI exposure
Action: Obtain explicit product/security sign-off that broadening the vault-enumeration button's visibility is intentional and risk-accepted; if not, reinstate a narrower, purpose-specific gate (which does not need to be a full second wallet-object probe — it can be a local UI-only flag) to preserve the ori
🔵 Diagnostic error message narrowed, reducing operator/SOC ability to distinguish specific wallet incompatibility reasons
Diagnostic error message narrowed, reducing operator/SOC ability to distinguish specific wallet incompatibility reasons
Action: Emit a structured error code or telemetry field distinguishing exactly which of walletProfile/proxyLogin/vaultList is missing, even if the user-facing prose message stays simplified.
⚪ Code simplification reduces surface for capability-check drift between two now-redundant functions
Code simplification reduces surface for capability-check drift between two now-redundant functions
Action: N/A
🧩 Affected Components
| Component | Impact | Change | What Changed |
|---|---|---|---|
| Admin UI Wallet Capability Detection (wallet.ts) | medium | modified | Two-tier capability detection (isWalletProxyAvailable + isWalletProfileAvailable) collapsed into a single-tier check; loginWithWalletProfile |
| Admin UI Login Page (Login.tsx) | medium | modified | Removed the profileAvailable-driven conditional rendering that gated the primary proxy-login button on a stricter check and adapted the seco |
📁 File Classifications
vtc-service/admin-ui/src/lib/wallet.ts
- Type: security
vtc-service/admin-ui/src/pages/Login.tsx
- Type: security
🛡️ STRIDE Threat Model
Identified Threats (10)
🟡 STRIDE-1: Capability Merge Enables Vault Enumeration UI Exposure in handleChooseIdentity
| Field | Detail |
|---|---|
| Category | Information Disclosure, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:L/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-284,CWE-668 |
| CAPEC | CAPEC-116,CAPEC-169 |
| OWASP | A01:2021 - Broken Access Control |
Description: handleChooseIdentity button in Login.tsx allows unauthorized vault enumeration exposure due to removal of the isWalletProfileAvailable() capability tier, resulting in the vault-listing consent UI now surfacing for a broader class of wallet states than intended
Evidence: vtc-service/admin-ui/src/pages/Login.tsx:265-296
{proxyAvailable && (
<>
<button onClick={handleProxyStart}>Sign in via VTA-proxied SIOP</button>
<button onClick={handleChooseIdentity}>Sign in as a different identity…</button>
</>
)}
Attack Scenario:
- Attacker controls or compromises a browser extension build that implements proxyLogin and vaultList but not walletProfile (or implements it inconsistently), satisfying only isWalletProxyAvailable().
- Victim visits the admin-ui Login.tsx page; proxyAvailable evaluates true because the old two-tier check (isWalletProxyAvailable() && isWalletProfileAvailable()) has been collapsed into a single isWalletProxyAvailable() gate (wallet.ts diff removing isWalletProfileAvailable).
- The UI now unconditionally renders both the 'Sign in via VTA-proxied SIOP' button and the 'Sign in as a different identity…' button in the same branch (Login.tsx JSX fragment), whereas previously the vault-choice path was gated behind the stricter profileAvailable check with an accurate exception-only message.
- Victim or attacker-influenced script clicks handleChooseIdentity, invoking window.vtaWallet.vaultList(), which per the removed code comment 'costs a consent prompt that enumerates the vault to this page.'
- The full DID vault (all bound identities) is enumerated to the page context even on wallet builds where the identity-resolution semantics were previously distinguished, increasing the surface on which vault contents are exposed to the web origin.
🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-002, EP-005
- Data Flows: Login.tsx -> window.vtaWallet.vaultList()
Preconditions: Attacker can influence or ship a wallet extension build lacking robust walletProfile semantics but exposing proxyLogin/vaultList, Victim interacts with the exposed 'choose identity' UI path, No server-side check re-validates which identity vault entries should be enumerable per origin
Existing Controls: Presence-detection gating via isWalletProxyAvailable() • User must click through a button (UI:P) to trigger vaultList()
Recommended Mitigations: Reinstate a distinct capability probe (or capability version negotiation) before exposing the vault-enumeration UI branch • Add origin-scoped restriction on vaultList() results at the wallet-extension layer • Add explicit user consent messaging distinguishing 'resolve identity' vs 'enumerate all vault entries' • Add telemetry/logging when vaultList() is invoked to detect anomalous enumeration patterns
🔵 STRIDE-2: Removed Method-Presence Check Enables TypeError-Induced Denial of Service in loginWithWalletProfile
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-476,CWE-755 |
| CAPEC | CAPEC-130 |
| OWASP | A05:2021 - Security Misconfiguration |
Description: loginWithWalletProfile in wallet.ts allows a client-side denial of service due to removal of the specific walletProfile function-presence check (isWalletProfileAvailable), resulting in an unhandled TypeError instead of a graceful error when a wallet exposes proxyLogin/vaultList but not walletProfile
Evidence: vtc-service/admin-ui/src/lib/wallet.ts:238-250
export async function loginWithWalletProfile(): Promise<VtaWalletLoginResult> {
if (!isWalletProxyAvailable()) {
throw new Error("VTA wallet doesn't expose proxy-login APIs.");
}
const rp = await rpDid();
const profile = await window.vtaWallet!.walletProfile!({
Attack Scenario:
- A wallet extension build exists (or is crafted by an attacker distributing a malicious/incomplete extension) that implements proxyLogin and vaultList (satisfying isWalletProxyAvailable) but does not implement walletProfile.
- Victim uses this wallet build against the admin-ui Login page; proxyAvailable is true so the 'Sign in via VTA-proxied SIOP' button renders and handleProxyStart is wired to eventually call loginWithWalletProfile().
- loginWithWalletProfile() now only checks isWalletProxyAvailable() (per the diff removing the walletProfile-specific isWalletProfileAvailable check) and proceeds to call
window.vtaWallet!.walletProfile!(...)at wallet.ts line ~245. - Because walletProfile is undefined on this wallet build, the non-null assertion (
!) forces a call on undefined, throwing an uncaught TypeError instead of the previous descriptive Error('This VTA wallet build cannot choose an identity for a site...'). - The login flow crashes ungracefully; busy/loading UI state (walletPhase.kind === 'running') may remain stuck, degrading availability of the login page for that session without a clear remediation message to the user.
🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-001, EP-003
- Data Flows: handleProxyStart -> loginWithWalletProfile -> window.vtaWallet.walletProfile()
Preconditions: Wallet extension exposes proxyLogin/vaultList but not walletProfile, User attempts the proxy-login flow via handleProxyStart
Existing Controls: isWalletProxyAvailable() still checks for proxyLogin, vaultList, and walletProfile function presence collectively
Recommended Mitigations: Wrap window.vtaWallet!.walletProfile!() call in a try/catch with a descriptive fallback error • Avoid non-null assertions on wallet-provided optional methods; use explicit runtime guards • Restore granular capability messaging distinguishing missing walletProfile vs missing proxyLogin/vaultList
🟠 STRIDE-3: Wallet-Origin Spoofing via window.vtaWallet Injection in isWalletAvailable Checks
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Information Disclosure |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | High |
| CWE | CWE-346,CWE-290 |
| CAPEC | CAPEC-151,CAPEC-194 |
| OWASP | A07:2021 - Identification and Authentication Failures |
Description: window.vtaWallet global object in isWalletAvailable/isWalletProxyAvailable allows wallet API spoofing due to lack of origin/integrity verification of the injected extension object, resulting in a malicious script or compromised extension impersonating the legitimate wallet and harvesting SIOP proxy-login credentials or minted bearer tokens
Evidence: vtc-service/admin-ui/src/lib/wallet.ts:1-30
export function isWalletAvailable(): boolean {
return (
typeof window.vtaWallet !== "undefined" && ...
);
}
Attack Scenario:
- Attacker compromises a browser extension, injects a malicious content script, or exploits an XSS elsewhere on the origin to define a fake window.vtaWallet object with walletProfile, proxyLogin, and vaultList functions before the legitimate extension initializes (or overriding it).
- Login.tsx calls isWalletAvailable() and isWalletProxyAvailable() (wallet.ts), which perform only typeof-based duck-typing checks with no cryptographic attestation of the object's origin or integrity.
- Victim clicks 'Sign in via VTA-proxied SIOP' (handleProxyStart) or 'Sign in as a different identity…' (handleChooseIdentity); Login.tsx invokes the attacker-controlled walletProfile()/proxyLogin()/vaultList() functions believing them to be the genuine wallet.
- The spoofed functions can return attacker-chosen DID/profile data to loginWithWalletProfile, or capture and exfiltrate any data the real page passes into proxyLogin (e.g., RP DID, nonce, challenge context) before optionally forwarding to the real extension or fabricating a bearer.
- If the returned VtaWalletLoginResult bearer is mirrored into the app's session (per code comment 'mirror it into the shared success tail'), the attacker can inject an arbitrary session token, achieving account takeover or session fixation against the admin UI.
🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-001, EP-002, EP-003, EP-004, EP-005
- Data Flows: window.vtaWallet.* -> Login.tsx session establishment
Preconditions: Attacker can execute script in the page context prior to or overriding the legitimate wallet's window.vtaWallet registration (e.g., via another XSS, malicious extension, or supply-chain compromise of a dependency), No Content Security Policy or object-integrity check prevents redefinition of window.vtaWallet
Existing Controls: None observed in the reviewed diff; presence checks are purely structural (typeof === 'function')
Recommended Mitigations: Implement a signed handshake/challenge-response so the page can verify the wallet extension's authenticity before trusting window.vtaWallet • Apply Object.freeze or defineProperty non-configurable guards where feasible, and detect redefinition attempts • Enforce a strict CSP to reduce script-injection vectors that could pre-empt window.vtaWallet • Validate bearer tokens returned from the wallet against expected issuer/audience/signature before establishing a session
🟡 STRIDE-4: Missing Rate Limiting on vaultList Enumeration Enables Repeated Vault Disclosure
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-799,CWE-200 |
| CAPEC | CAPEC-127 |
| OWASP | A01:2021 - Broken Access Control |
Description: handleChooseIdentity entry point in COMP-002 allows repeated vault enumeration due to absence of any throttling or one-time-consent enforcement around vaultList(), resulting in progressive disclosure of all DID identities bound in the user's wallet to a compromised or malicious page context
Evidence: vtc-service/admin-ui/src/pages/Login.tsx:280-296
<button type="button" className="link" onClick={handleChooseIdentity} disabled={busy}>Sign in as a different identity…</button>
Attack Scenario:
- Following the capability-merge change, the 'Sign in as a different identity…' button (handleChooseIdentity) is now reachable in more wallet states (see STRIDE-1).
- A malicious script embedded on the admin-ui origin (e.g., via a supply-chain-compromised dependency loaded into admin-ui) programmatically triggers handleChooseIdentity repeatedly or directly calls window.vtaWallet.vaultList() if reachable from page JS.
- Each invocation surfaces a consent prompt per the code comment, but if the user has previously granted a broad or session-scoped consent, subsequent calls may not re-prompt, allowing repeated silent enumeration.
- The attacker aggregates vault entries returned across multiple calls/sessions to build a complete map of the victim's bound identities across relying parties.
- This identity map is used for targeted phishing, cross-RP correlation, or selection of the most privileged identity for a follow-on impersonation attack via loginWithWalletProxy.
🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-002, EP-005
- Data Flows: handleChooseIdentity -> window.vtaWallet.vaultList()
Preconditions: Malicious or compromised script running in admin-ui page context, Wallet extension consent model allows session-scoped or silent repeat approval of vaultList()
Existing Controls: Per-invocation consent prompt (per code comments) at the wallet-extension layer
Recommended Mitigations: Enforce explicit, non-cacheable per-request consent for vaultList() at the extension layer • Add page-level rate limiting/backoff on repeated identity-chooser invocations • Minimize vault data returned (e.g., opaque handles rather than full DID/profile metadata) until an identity is actually selected
🟠 STRIDE-5: Unauthenticated Header-Exempt Wallet API Base Enables CSRF-Style Bearer Injection
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-352 |
| CAPEC | CAPEC-62 |
| OWASP | A01:2021 - Broken Access Control |
Description: walletApiBase() same-origin header-exempt wallet endpoint in COMP-001 allows cross-site request forgery style abuse due to the endpoint being explicitly exempted from standard header checks, resulting in unauthorized bearer minting or session establishment on behalf of a victim without their consent
Evidence: vtc-service/admin-ui/src/lib/wallet.ts:32-36
/** API base for the wallet's auth round-trip. Points at the VTC's
* header-exempt wallet surface, served same-origin with the admin UI. */
function walletApiBase(): string {
Attack Scenario:
- walletApiBase() (wallet.ts) documents that it 'points at the VTC's header-exempt wallet surface, served same-origin with the admin UI,' implying standard CSRF-protective headers (e.g., custom X- headers, Origin checks) are not enforced on this endpoint.
- Attacker lures an authenticated admin-ui victim to a malicious page while their wallet extension session/cookies are still active.
- The malicious page issues a cross-origin (or same-site via subdomain) request pattern that mimics the wallet's round-trip call to walletApiBase(), since the endpoint is exempted from the header validation that would normally block forged requests.
- Because the endpoint is header-exempt specifically to accommodate the browser-extension round-trip, standard CSRF defenses (SameSite cookies aside) may be bypassed, allowing the attacker to trigger wallet-auth-adjacent operations or replay captured proxyLogin/bearer flows against the victim's session.
- If successful, the attacker obtains or forces issuance of a valid bearer token bound to the victim's session, enabling account takeover of the admin UI.
🔎 Threat Clue: Derived from COMP-001 via EP-006
- Data Flows: Login.tsx/wallet.ts -> walletApiBase() HTTP endpoint
Preconditions: Endpoint genuinely lacks Origin/Referer or custom-header CSRF validation as implied by 'header-exempt', Victim has an active session/cookie for the admin-ui/VTC origin, Attacker can lure victim to a crafted page or exploit a same-site subdomain
Existing Controls: Same-origin serving reduces trivial cross-origin exploitation for browsers respecting SOP • Bearer-based auth (EP-006 marked auth_required=true) may still require a valid credential
Recommended Mitigations: Apply SameSite=Strict/Lax cookies and re-validate Origin/Referer even on the header-exempt path • Require a fresh, single-use nonce/challenge from the server for each wallet round-trip to prevent replay • Scope the header exemption as narrowly as possible and document/audit why it is needed • Add explicit anti-CSRF tokens bound to the wallet round-trip flow
🔵 STRIDE-6: Insufficient Logging of Proxy-Login and Vault Enumeration Actions Enables Repudiation
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Low |
| Likelihood | Likely |
| CVSS | 3.8 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N |
| Residual Severity | Low |
| CWE | CWE-778 |
| CAPEC | CAPEC-93 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: handleProxyStart and handleChooseIdentity entry points in Login.tsx allow repudiation of identity-selection and proxy-login actions due to absence of any observed client- or server-side audit logging for these sensitive operations, resulting in an inability to forensically verify which identity or vault entry was used to authenticate a given admin session
Evidence: vtc-service/admin-ui/src/pages/Login.tsx:265-296
const busy = phase.kind === "running" || walletPhase.kind === "running";
Attack Scenario:
- A user (or attacker who has taken over the session per STRIDE-3/STRIDE-5) invokes handleChooseIdentity and selects an arbitrary vault entry via vaultList()/walletProfile().
- loginWithWalletProxy or loginWithWalletProfile mints a bearer bound to the chosen identity with no client-side record of which DID/profile was selected beyond transient UI state.
- No log entry ties the resulting authenticated admin session back to the specific vault entry chosen, the wallet extension version, or a timestamped consent event.
- If the session is later found to have performed unauthorized administrative actions, the true operator cannot be reliably attributed, and a malicious actor who chose a different identity can plausibly deny responsibility.
- Incident response and compliance audits (e.g., SOC2, admin action attribution) are hampered by the missing linkage between identity selection and session issuance.
🔎 Threat Clue: Derived from COMP-002, COMP-001 via EP-001, EP-002
- Data Flows: handleChooseIdentity -> loginWithWalletProfile -> bearer issuance
Preconditions: No server-side audit trail correlating vault/profile selection with session/bearer issuance, Multiple identities available for selection via the proxy path
Existing Controls: UI-level consent prompts (per code comments) provide some human-observable friction but no durable record
Recommended Mitigations: Log identity-selection events (DID, timestamp, wallet extension version) server-side at bearer-mint time • Bind issued bearer tokens to an auditable claim identifying the selected vault entry • Expose an admin-facing audit log of login/identity-selection events
🔵 STRIDE-7: Non-Null Assertion Misuse on Optional Wallet Methods Causes Unhandled Exception Propagation
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 2.9 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-367,CWE-476 |
| CAPEC | CAPEC-26 |
| OWASP | A05:2021 - Security Misconfiguration |
Description: loginWithWalletProfile in wallet.ts allows unhandled exception propagation due to pervasive use of TypeScript non-null assertions (window.vtaWallet!.walletProfile!) instead of runtime validation, resulting in unpredictable crash states across the wallet login flow whenever the extension's API surface partially degrades between the capability check and the call site
Evidence: vtc-service/admin-ui/src/lib/wallet.ts:245-249
const profile = await window.vtaWallet!.walletProfile!({
Attack Scenario:
- Between the isWalletProxyAvailable() check and the actual call to window.vtaWallet!.walletProfile!(...), the extension's injected object could be mutated (e.g., by extension update, race condition, or malicious script redefining properties, per STRIDE-3) such that walletProfile is removed or altered (TOCTOU window).
- The TypeScript non-null assertion operator (!) suppresses compiler null-checks but performs no runtime guard, so if window.vtaWallet or walletProfile becomes undefined between check and use, a TypeError is thrown at call time.
- This exception is not caught locally in the shown code path, propagating up through the async call chain of loginWithWalletProfile.
- Depending on the caller's error handling (handleProxyStart / handleChooseIdentity in Login.tsx), the walletPhase state machine may become stuck in 'running', permanently disabling the login button (busy=true) until a manual page reload.
- Repeated triggering of this TOCTOU race by an attacker capable of manipulating extension state amounts to a persistent client-side denial of service against the admin login page.
🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-001, EP-002
- Data Flows: Login.tsx handlers -> wallet.ts loginWithWalletProfile
Preconditions: Attacker or race condition can alter window.vtaWallet's method surface between the capability check and the actual invocation, No try/catch wraps the wallet API invocation in the visible code
Existing Controls: Initial isWalletProxyAvailable() presence check reduces but does not eliminate the TOCTOU window
Recommended Mitigations: Replace non-null assertions with explicit runtime guards immediately before each wallet API invocation • Wrap all window.vtaWallet.* calls in try/catch with state-machine recovery (reset walletPhase to 'idle' or 'error') • Add a watchdog timeout to reset busy/running UI state if the wallet call does not resolve
⚪ STRIDE-8: Diagnostic Message Degradation Reduces User Ability to Detect Wallet Downgrade or Tampering
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Informational |
| Likelihood | Likely |
| CVSS | 1.5 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-778 |
| CAPEC | CAPEC-118 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: loginWithWalletProfile error path allows reduced attacker/anomaly detectability due to the replacement of a specific, actionable error message with a generic one, resulting in the user (and any security telemetry parsing error text) losing signal that would have distinguished a genuinely outdated wallet from a maliciously crippled one
Evidence: vtc-service/admin-ui/src/lib/wallet.ts:238-243
if (!isWalletProxyAvailable()) {
throw new Error("VTA wallet doesn't expose proxy-login APIs.");
}
Attack Scenario:
- Previously, isWalletProfileAvailable()'s failure path produced a specific message: 'This VTA wallet build cannot choose an identity for a site. Update the extension, or pin a did-self-issued vault entry to this VTC by hand.'
- The refactor replaces this with a generic 'VTA wallet doesn't expose proxy-login APIs.' regardless of whether the missing capability is walletProfile specifically or the whole proxy surface.
- An operator or SOC analyst reviewing client-side error logs/telemetry can no longer distinguish 'wallet is merely outdated for identity resolution' from 'wallet is broken/compromised in a broader way,' delaying incident triage.
- An attacker who has partially neutered a victim's wallet extension (e.g., via a malicious update stripping walletProfile only) benefits from this reduced diagnostic granularity, as the resulting user-facing/log message no longer hints at that specific capability gap.
- This reduces detection fidelity for anomaly-based monitoring that might otherwise flag 'identity-resolution capability suddenly missing' as a distinguishable event from generic proxy unavailability.
🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-001
- Data Flows: loginWithWalletProfile error path
Preconditions: Client-side error telemetry or user reporting is relied upon to detect wallet tampering, No structured/coded error identifiers are emitted alongside the message
Existing Controls: An error is still thrown and surfaced to the user, preserving basic failure signaling
Recommended Mitigations: Emit structured error codes (not just prose messages) distinguishing which specific capability is missing • Retain granular internal logging even if the user-facing message is simplified • Include capability-diff telemetry (which of walletProfile/proxyLogin/vaultList are missing) in client analytics
🔵 STRIDE-9: Prompt-Injection-Style Instructional Comments in Removed Code Blocks Targeting Downstream LLM Reviewers
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 2.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-1059 |
| CAPEC | CAPEC-176 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: removed-code comment blocks in the diff allow potential downstream instruction confusion due to natural-language explanatory prose embedded directly in source comments describing security rationale, resulting in a risk that automated LLM-based code reviewers or security scanners could be steered by carefully worded comments rather than by actual code behavior
Evidence: vtc-service/admin-ui/src/lib/wallet.ts:126-140
/** True iff the wallet exposes the whole proxy-login surface this page drives:
* `walletProfile` to resolve the identity, `proxyLogin` to mint as it, and
* `vaultList` for the pick-a-different-identity path. ... */
Attack Scenario:
- The diff contains extensive prose-style comments explaining security rationale (e.g., 'Presence detection, not version negotiation,' 'the difference is worth an accurate message rather than a TypeError').
- An attacker submitting a future PR could craft similarly persuasive comments that assert a security property is 'safe' or 'intended' (e.g., 'this is fine because X') to bias human or automated (LLM-assisted) code review into approving a change that removes a real security control.
- This PR itself contains only descriptive/benign prose (verified as data, not an injected instruction to any analysis tool), but it establishes a project norm/precedent of long persuasive comments justifying capability-check removals.
- A future contributor (malicious insider or compromised account) could exploit this normalized pattern to embed a comment that convinces a reviewer (human or LLM-assisted linting/security tool) that a dangerous removal (e.g., disabling a signature check) is merely a 'presence-detection simplification.'
- If accepted, this results in silent removal of a genuine security control, since reviewers were socially engineered via comment prose rather than evaluating the actual runtime logic change.
🔎 Threat Clue: Derived from COMP-002, COMP-003
Preconditions: Project culture accepts long persuasive prose comments as sufficient security justification without independent verification, Automated review tooling (including LLM-based reviewers) is used without out-of-band verification of comment claims against actual code semantics
Existing Controls: This specific PR's comments were verified against actual code behavior and found consistent with a genuine (if imperfect) simplification, not a malicious injection
Recommended Mitigations: Require security-relevant capability-check removals to be justified with test coverage, not solely prose comments • Train/configure automated review tooling to treat in-code comments as untrusted narrative, always validating against actual control-flow • Add a lightweight security-review checklist item: 'Does this removal reduce a distinct capability check? If so, was equivalent protection preserved elsewhere?'
🟡 STRIDE-10: Race Condition Between Capability Detection and Proxy Login Invocation Enables TOCTOU Bypass
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-367 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: isWalletProxyAvailable check in Login.tsx render path allows a time-of-check-time-of-use bypass due to the boolean capability flags (walletAvailable, proxyAvailable) being computed once per render rather than re-validated at click time, resulting in stale-capability-based invocation of wallet APIs that may no longer be safe or present
Evidence: vtc-service/admin-ui/src/pages/Login.tsx:53-56
const walletAvailable = isWalletAvailable();
const proxyAvailable = isWalletProxyAvailable();
const busy = phase.kind === "running" || walletPhase.kind === "running";
Attack Scenario:
- Login.tsx computes walletAvailable and proxyAvailable once during component render/execution (const proxyAvailable = isWalletProxyAvailable();), not inside the click handlers themselves.
- Between render and the user's click on 'Sign in via VTA-proxied SIOP' or 'Sign in as a different identity…', the wallet extension could be uninstalled, updated, disabled, or have its API surface changed (e.g., due to an extension auto-update or a malicious extension swap).
- The stale proxyAvailable=true flag still gates rendering of the buttons and their onClick handlers (handleProxyStart, handleChooseIdentity), which is not re-checked before invoking the underlying wallet.ts functions.
- handleProxyStart/handleChooseIdentity call into loginWithWalletProxy/loginWithWalletProfile, which internally re-check isWalletProxyAvailable() but this happens post-click, meaning the UI already presented an affordance that may momentarily mismatch the actual wallet state, and any state read in between (e.g., partial extension re-initialization) could yield inconsistent behavior.
- In edge cases, this stale-state window could be leveraged (in combination with STRIDE-3's spoofing) to have the UI display and invoke handlers against a since-swapped window.vtaWallet object, since React does not force a re-render/re-check of these booleans on every interaction.
🔎 Threat Clue: Derived from COMP-002 via EP-001, EP-002
- Data Flows: Render-time capability check -> click-time invocation
Preconditions: Wallet extension state changes between component render and user click, No re-validation of capability immediately before the sensitive API call beyond the internal isWalletProxyAvailable() re-check in wallet.ts functions
Existing Controls: wallet.ts functions (loginWithWalletProxy, loginWithWalletProfile) do perform their own isWalletProxyAvailable() check at call time, partially mitigating stale UI state
Recommended Mitigations: Re-evaluate wallet capability immediately before dispatching to window.vtaWallet inside the click handler, not relying solely on render-time booleans • Subscribe to extension lifecycle events (if available) to invalidate cached capability flags reactively • Add integrity pinning of the wallet object reference captured at first detection to detect object substitution
🍝 PASTA Threat Model
Application Purpose
The Admin UI wallet-login flow enables VTC (Verifiable Trust Consortium) operators to authenticate to an administrative interface using a browser-extension-based DID wallet via WebAuthn and SIOP (Self-Issued OpenID Provider) proxy login, providing decentralized-identity-based administrative access control for the trust infrastructure.
Inherent Risks
- Browser-extension-based wallets rely on client-side JavaScript object injection (window.vtaWallet) that is inherently susceptible to spoofing without cryptographic attestation.
- Decentralized identity vault enumeration inherently exposes multiple bound identities to the requesting web origin, creating a privacy/correlation risk by design.
- Same-origin, header-exempt authentication endpoints trade CSRF hardening for browser-extension compatibility.
Objectives
Risk: Limit blast radius of a compromised or malicious wallet extension build; Maintain accurate risk signal in error messages and logs for anomaly detection
Business: Provide VTC operators secure, decentralized-identity-based administrative access; Support multiple identity-selection UX paths for operators managing multiple DID entries
Security: Prevent unauthorized session establishment via spoofed or malicious wallet objects; Protect vault/identity enumeration data from unauthorized exposure; Ensure login flow failures degrade safely without indefinite UI lockup
Financial: Avoid costs associated with account-takeover incidents against administrative infrastructure; Minimize support burden from confusing wallet-compatibility error states
Compliance: Maintain auditable attribution of administrative session origin to a specific identity; Support incident response requirements for administrative access logging
Functional: Detect wallet extension capability tiers reliably (base, proxy, profile-resolution); Allow login via primary wallet-bound identity or explicit identity selection
Operational: Maintain reliable login flow across varying wallet extension versions; Ensure UI does not present non-functional buttons for unsupported wallet builds
Business Impact Analysis (1)
BIA-1: Administrative Wallet-Based Authentication (Critical)
VTC operators authenticate to the admin UI using a DID wallet browser extension via WebAuthn/SIOP proxy login to gain administrative control over trust infrastructure configuration.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 01:00 hours
- Stakeholders: Admin UI Operators / Compliance/Audit Team / End Relying Parties / Security Engineering Team / VTC Platform Owners
- Dependencies: Browser Extension (VTA Wallet) / SIOP Proxy Login Service / WebAuthn Subsystem / Wallet API (window.vtaWallet) / Wallet Header-Exempt HTTP Endpoint
- Disruptions: Malicious or spoofed wallet object impersonation / Uncaught TypeError crashing the login flow / CSRF-style forged request against the header-exempt wallet endpoint / Vault enumeration abuse exposing all bound identities
- Impacts: Unauthorized administrative access to trust infrastructure / Loss of operator trust in the wallet-login mechanism / Regulatory exposure if administrative access logs cannot attribute sessions to identities / Support/helpdesk cost increase from broken login states
Technical Scope
Roles (2): RO-1 VTC Administrator · RO-2 Anonymous Visitor
Actors (3): AC-1 VTC Operator · AC-2 VTA Wallet Extension Process · AC-3 VTC Wallet Auth Service
Entry Points (6): EP-001 Sign in via VTA-proxied SIOP Button · EP-002 Sign in as a Different Identity Button · EP-003 Wallet Profile Resolution API · EP-004 Wallet Proxy Login API · EP-005 Wallet Vault List API · EP-006 Wallet Auth HTTP Round-Trip Endpoint
Threat Actors (3): TA-1 Malicious Browser Extension Author · TA-2 Web-Based Attacker (XSS/Supply-Chain) · TA-3 Malicious Insider Contributor
Infrastructure (1): IF-1 Admin UI Static Hosting
Trust Boundaries (3): TB-1 Browser Page Context to Extension Boundary · TB-2 Browser to VTC Service Boundary · TB-3 VTC Internal Service Boundary
External Entities (1): EE-1 VTA Wallet Browser Extension
System Components (3): COMP-001 VTC Wallet Auth HTTP Endpoint · COMP-002 Admin UI Login Page · COMP-003 Browser Extension Wallet API
Resources And Assets (3): RA-1 DID Vault Entries · RA-2 Session Bearer Token · RA-3 Wallet Capability State
Technologies And Dependencies (3): TD-1 React · TD-2 WebAuthn Browser API · TD-3 VTA Wallet Extension API
Use Cases (2)
- Primary Wallet-Bound Identity Login: A VTC operator clicks 'Sign in via VTA-proxied SIOP' to authenticate using whichever DID identity the wallet extension has already bound to the admin UI origin.
- Explicit Identity Selection via Vault Chooser: An operator holding multiple bound identities clicks 'Sign in as a different identity…' to enumerate the wallet vault and explicitly select which DID to authenticate with.
📋 Risk Registry (6)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Spoofed or malicious wallet extension impersonates window.vtaWallet to harvest identities and session bearers | High | High | Immediate | High |
| RISK-002 | Header-exempt wallet auth endpoint is susceptible to CSRF-style forged request replay | High | Medium | Immediate | Medium |
| RISK-003 | Capability-check merge widens exposure of vault-enumeration UI, increasing identity-disclosure surface | Medium | Medium | Short-Term | Medium |
| RISK-004 | Non-null assertions and stale render-time capability flags create TOCTOU-driven denial of service | Low | Low | Medium-Term | Low |
| RISK-005 | Insufficient audit logging of identity selection and proxy login prevents forensic attribution | Low | Low | Medium-Term | Low |
| RISK-006 | Persuasive prose comments in security-relevant diffs could normalize social-engineering of code review | Low | None | Long-Term | Low |
⚔️ Attack Scenarios (3)
SC-3: Browser Extension Wallet API
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC3@{ shape: rect, label: "SC-3: Browser Extension Wallet API" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE346@{ shape: rect, label: "CWE-346: Origin Validation Error" }
CWE799@{ shape: rect, label: "CWE-799: Improper Control of Interaction Frequency" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC151@{ shape: rect, label: "CAPEC-151: Identity Spoofing" }
CAPEC127@{ shape: rect, label: "CAPEC-127: Directory Indexing" }
end
subgraph SL4["4. Threats"]
direction LR
S3@{ shape: rect, label: "STRIDE-3: Wallet-Origin Spoofing<br><i>High / Possible</i>" }
S4@{ shape: rect, label: "STRIDE-4: Vault Enumeration Disclosure<br><i>Medium / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Browser Extension Author<br><i>Harvest DID identities</i>" }
TA2@{ shape: rect, label: "TA-2: Web-Based Attacker<br><i>Session takeover</i>" }
end
SC3 --> CWE346
CWE346 --> CAPEC151
CAPEC151 --> S3
S3 --> TA1
SC3 --> CWE799
CWE799 --> CAPEC127
CAPEC127 --> S4
S4 --> TA2
linkStyle 0 stroke:#FF0000,stroke-width:2px
linkStyle 1 stroke:#FF0000,stroke-width:2px
linkStyle 2 stroke:#FF0000,stroke-width:2px
linkStyle 3 stroke:#FF0000,stroke-width:2px
linkStyle 4 stroke:#FFA500,stroke-width:2px
linkStyle 5 stroke:#FFA500,stroke-width:2px
linkStyle 6 stroke:#FFA500,stroke-width:2px
linkStyle 7 stroke:#FFA500,stroke-width:2px
SC-1: VTC Wallet Auth HTTP Endpoint
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC1@{ shape: rect, label: "SC-1: VTC Wallet Auth HTTP Endpoint" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE352@{ shape: rect, label: "CWE-352: Cross-Site Request Forgery" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC62@{ shape: rect, label: "CAPEC-62: Cross Site Request Forgery" }
end
subgraph SL4["4. Threats"]
direction LR
S5@{ shape: rect, label: "STRIDE-5: CSRF-Style Bearer Injection<br><i>High / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Web-Based Attacker<br><i>Session takeover</i>" }
end
SC1 --> CWE352
CWE352 --> CAPEC62
CAPEC62 --> S5
S5 --> TA2
linkStyle 0 stroke:#FF0000,stroke-width:2px
linkStyle 1 stroke:#FF0000,stroke-width:2px
linkStyle 2 stroke:#FF0000,stroke-width:2px
linkStyle 3 stroke:#FF0000,stroke-width:2px
SC-2: Admin UI Login Page
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC2@{ shape: rect, label: "SC-2: Admin UI Login Page" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE284@{ shape: rect, label: "CWE-284: Improper Access Control" }
CWE367@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
CWE778@{ shape: rect, label: "CWE-778: Insufficient Logging" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC116@{ shape: rect, label: "CAPEC-116: Excavation" }
CAPEC26@{ shape: rect, label: "CAPEC-26: Leveraging Race Conditions" }
CAPEC93@{ shape: rect, label: "CAPEC-93: Log Injection-Tampering" }
end
subgraph SL4["4. Threats"]
direction LR
S1@{ shape: rect, label: "STRIDE-1: Vault Enumeration UI Exposure<br><i>Medium / Likely</i>" }
S10@{ shape: rect, label: "STRIDE-10: TOCTOU Bypass<br><i>Medium / Possible</i>" }
S6@{ shape: rect, label: "STRIDE-6: Repudiation of Identity Selection<br><i>Low / Likely</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Browser Extension Author<br><i>Harvest DID identities</i>" }
TA2@{ shape: rect, label: "TA-2: Web-Based Attacker<br><i>Session takeover</i>" }
end
SC2 --> CWE284
CWE284 --> CAPEC116
CAPEC116 --> S1
S1 --> TA1
SC2 --> CWE367
CWE367 --> CAPEC26
CAPEC26 --> S10
S10 --> TA2
SC2 --> CWE778
CWE778 --> CAPEC93
CAPEC93 --> S6
S6 --> TA1
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#FFA500,stroke-width:2px
linkStyle 2 stroke:#FFA500,stroke-width:2px
linkStyle 3 stroke:#FFA500,stroke-width:2px
linkStyle 4 stroke:#FFA500,stroke-width:2px
linkStyle 5 stroke:#FFA500,stroke-width:2px
linkStyle 6 stroke:#FFA500,stroke-width:2px
linkStyle 7 stroke:#FFA500,stroke-width:2px
linkStyle 8 stroke:#00FF00,stroke-width:2px
linkStyle 9 stroke:#00FF00,stroke-width:2px
linkStyle 10 stroke:#00FF00,stroke-width:2px
linkStyle 11 stroke:#00FF00,stroke-width:2px
📊 Risk Summary
Total Threats: 10
By Severity: Low: 4 · High: 2 · Medium: 3 · Informational: 1
By Category: Information Disclosure: 4 · Elevation of Privilege: 1 · Denial of Service: 3 · Spoofing: 2 · Tampering: 4 · Repudiation: 1
🎯 Attack Surface
Kill Chain 1: An attacker who compromises or distributes a malicious browser extension build (TA-1) can define a spoofed window.vtaWallet object (STRIDE-3, CWE-346) that passes the purely duck-typed isWalletAvailable/isWalletProxyAvailable checks in wallet.ts; when the victim clicks 'Sign in via VTA-proxied SIOP' or 'Sign in as a different identity…' in Login.tsx (EP-001/EP-002), the spoofed object's walletProfile/proxyLogin/vaultList functions are invoked in place of the genuine wallet, allowing harvesting of DID profile data and any bearer subsequently mirrored into the admin session — a direct path from client-side object injection to administrative account takeover. Kill Chain 2: The capability-check simplification (removal of isWalletProfileAvailable, STRIDE-1) widens the conditions under which the 'Sign in as a different identity…' button renders, increasing the population of wallet states from which vaultList() (EP-005) can be invoked; chained with the absence of rate limiting or one-time-consent enforcement (STRIDE-4), a malicious script running in the admin-ui origin (via a supply-chain-compromised dependency, TA-2) can repeatedly enumerate the full DID vault (RA-1) across sessions, building a correlation map used to select the highest-privilege identity for a follow-on impersonation attempt via loginWithWalletProxy. Kill Chain 3: The same-origin, header-exempt wallet auth HTTP endpoint (SC-1, STRIDE-5, CWE-352) can be combined with either of the above chains: once an attacker has captured or fabricated a wallet-profile response via a spoofed extension (Kill Chain 1), they can replay the wallet-auth round-trip against the header-exempt endpoint without the CSRF protections applied elsewhere in the service, completing bearer issuance and achieving full session takeover of the admin UI — demonstrating how a client-side spoofing primitive and a server-side CSRF gap compose into a critical, cross-boundary account-takeover chain. Kill Chain 4: Independently, the pervasive use of non-null assertions on optional wallet methods (STRIDE-7, CWE-476) combined with stale render-time capability flags (STRIDE-10, CWE-367) creates a lower-severity but persistent availability risk: an extension update or malicious swap occurring between component render and button click can leave the login flow in a permanently 'busy' state, and because errors are now reported with a generic message (STRIDE-8), operators and SOC analysts lose the diagnostic granularity needed to distinguish benign wallet incompatibility from active tampering, delaying detection of the more severe spoofing chains above.
🛡️ Risk Mitigation Strategy
Priority 1 (Immediate): Address the two High-severity, cross-boundary risks first — RISK-001 (wallet spoofing via unauthenticated window.vtaWallet duck-typing) and RISK-002 (CSRF-style replay against the header-exempt wallet auth endpoint) — because together they form the most direct path to full administrative account takeover. This requires introducing a cryptographic handshake or signed attestation for the wallet extension's identity, server-side validation of bearer issuer/audience/signature, and reinstatement of Origin/Referer or nonce-based validation on the wallet auth round-trip even though it is served same-origin. Priority 2 (Short-Term): Remediate RISK-003, the capability-check merge that widened vault-enumeration UI exposure, by reinstating a distinct, accurately-scoped capability probe (equivalent to the removed isWalletProfileAvailable) before rendering the 'choose a different identity' affordance, and by enforcing non-cacheable, per-request consent at the extension layer so that repeated vaultList() calls cannot silently build a full identity map. Priority 3 (Medium-Term): Harden the client-side robustness of the login flow by replacing non-null assertions with explicit runtime guards (RISK-004), wrapping all window.vtaWallet.* invocations in try/catch with proper walletPhase state recovery, and re-validating capability immediately before each sensitive call rather than relying on stale render-time booleans — this closes the TOCTOU window that could be chained with a spoofing attack. In parallel, restore structured, code-based error reporting and add server-side audit logging that binds issued bearers to the specific DID/vault entry selected (RISK-005), giving both users and SOC tooling the diagnostic and forensic signal that was lost in this refactor. Priority 4 (Long-Term): Institutionalize a review practice (RISK-006) requiring that any PR removing or merging a security-relevant capability check be accompanied by test coverage and an explicit state
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 2 | 1 |
Confirmed (2)
- 🟡 Broken Access Control: Merged capability check exposes vault-enumeration UI to broader wallet states
- 🟡 Unhandled TypeError via non-null assertion after removal of walletProfile-specific presence check (triaged LOW→MEDIUM)
Must-Review-By-Human (1)
- 🟠 Header-exempt same-origin wallet API endpoint potentially bypasses CSRF protections
Follow-up to #1212.
isWalletProfileAvailable()existed so that PR could merge before the wallet method it depends on did — the "land in either order" property. OpenVTC/vta-browser-plugin#145 has now shipped, so the second probe describes a wallet build that does not exist: every extension that exposesproxyLoginexposeswalletProfiletoo.Folded into
isWalletProxyAvailable(), which now checks the three methods this page actually calls.What goes
profileAvailable— an arm for a wallet nobody has.loginWithWalletProfile's "This VTA wallet build cannot choose an identity for a site. Update the extension…" message, which can no longer be reached.What stays
Presence detection, which is not the same thing — the extension may simply not be installed, and that is why the buttons are gated at all. The probe now just asks for everything the page uses instead of splitting into two questions with one possible answer.
"Sign in as a different identity…" and the entry picker behind it. That is a real capability for an operator holding more than one persona at this VTC, not a compatibility arm, and it stays behind an explicit click because reaching it discloses the vault to this page.
Nothing about the flow changes:
walletProfile→/auth/challenge→proxyLoginis untouched.Checks
tsc -b --noEmitcleannpm run buildcleanisWalletProfileAvailable/profileAvailableremain