Skip to content

fix(console): the persona pane no longer locks out the recommended setup - #183

Merged
stormer78 merged 1 commit into
mainfrom
fix/persona-holder-gate
Sep 7, 2026
Merged

fix(console): the persona pane no longer locks out the recommended setup#183
stormer78 merged 1 commit into
mainfrom
fix/persona-holder-gate

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Found while sweeping the persona vocabulary across the three repos. This one is
not cosmetic — the console currently disables itself for the configuration the
stack now recommends.

The stale claim

holderGate disabled every button on the persona pane unless the caller was an
unscoped admin, and said so:

Your identity sits above every trust context, so this needs an unscoped holder
credential — Admin at this agent with no context restriction.

True when written. Not true since Capability::PersonaHolder
(verifiable-trust-infrastructure#1286): a context-scoped entry granted that
capability reaches the holder-scoped tasks too — and it is now the recommended
shape, because OpenVTC's setup asks for exactly it (--admin-holder).

So an operator following the documented path saw a dead pane and a message
telling them to widen their agent credential, when a capability grant is the
better answer and the one they already had.

What it does now

isUnscopedHolder is sufficient and no longer necessary. auth/whoami
reports roles and scopes but not capabilities, so this console cannot tell which
a caller holds — and the thing it must not do is claim to know.

The caution now names both ways to satisfy the agent, says plainly that the
console cannot see which you have, and stands aside rather than disabling.
The agent decides on every task regardless — isUnscopedHolder's own comment
said as much before this change.

It also speaks design-docs/persona-vocabulary.md: your facts, not "the
holder's attribute pool".

Where it lives

Moved to manager/holder-gate.ts with isUnscopedHolder beside it — one
decision, one module — following the precedent the consent view already sets:
what the screen says here is a security property, and nothing tests a
component's reasoning. Concretely, the pane's test runner cannot load a .tsx
file, so a decision left inside one is a decision no test can reach. use-vta.ts
re-exports the predicate; importers are unaffected.

manager-holder-gate.test.mts pins all four cases, including that the caution
names persona-holder and the unscoped credential — naming only one is what
made the old message wrong — and that it carries no word the vocabulary keeps
off the screen.

Follow-up

Making this certain rather than cautious means auth/whoami returning
capabilities: a spec change, then trust-tasks-rs, then the VTA. Worth doing, and
not worth guessing in the meantime.

npm run lint (tsc -b) clean; npm test 851 passing across the three
workspaces.

`holderGate` disabled every button on the pane unless the caller was an unscoped
admin, and explained that this was the only credential that could reach the
holder-scoped tasks. That was true when it was written and stopped being true
when the agent gained `persona-holder`
(verifiable-trust-infrastructure#1286): a context-scoped entry granted that
capability reaches them too — and it is now the *recommended* shape, since
OpenVTC's setup asks for exactly it.

So the console was disabling itself for the configuration the stack now tells
people to create, under a message sending them to widen a credential when a
capability grant is the better answer.

`isUnscopedHolder` is sufficient and no longer necessary, and `auth/whoami`
reports roles and scopes but not capabilities, so this console cannot tell which
a caller holds. What it must not do is claim to know. It now explains what the
task needs, names both ways to satisfy it, says plainly that it cannot see which
you have — and stands aside. The agent was always the one deciding; the comment
on `isUnscopedHolder` said so already.

The caution also speaks the vocabulary now (`design-docs/persona-vocabulary.md`):
your *facts*, not "the holder's attribute pool".

## Where it lives

Moved to `manager/holder-gate.ts`, with `isUnscopedHolder` alongside it — one
decision, one module. The reason is the one the consent view already follows:
what the screen says here is a security property, and nothing tests a
component's reasoning. The pane's test runner cannot load a `.tsx` file, so a
decision left inside one is a decision no test can reach. `use-vta.ts`
re-exports the predicate, so importers are unaffected.

## Follow-up

Making this certain again means `auth/whoami` returning capabilities — a spec
change, then trust-tasks-rs, then the VTA. Worth doing; not worth guessing in
the meantime.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 131f3bd into main Sep 7, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/persona-holder-gate branch September 7, 2026 14:14
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

8 AI-confirmed issues.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #183

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/persona-holder-gatemain
Validated 2026-09-07
Scan ID d12c8b87
Validator AI Security Validation Agent

🗺️ Scan Coverage

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

Module Files scanned Findings
packages/extension 6 12

⚖️ Cross-Finding Reconciliation

1 root-cause cluster(s) received divergent verdicts across findings that share the same file + weakness. These are surfaced (not auto-resolved) — a reviewer should confirm the verdicts are intentionally different, not an artifact of findings being judged in isolation:

  • packages/extension/src/manager/holder-gate.ts (CWE-200) — verdicts: validated, duplicate
    • validated: Authorization-relevant rationale exposed verbatim to client via holderGate() message revealing internal capability model
    • duplicate: Persona pool caution message informational disclosure of internal authorization model

Executive Summary

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

🔒 Security Issues

Confirmed Vulnerabilities (8)

🟡 UI-only advisory replaces enforced access control on destructive persona operations (client-side authorization bypass)

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/holder-gate.ts:44
Finding ID github_pr-e03f62d92e2c
CWE CWE-862
OWASP A01:2021 - Broken Access Control
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — The vulnerable code (holderGate) only controls whether a UI tooltip/warning is shown — it never performed real authorization itself; per the code comments and attack scenario, the actual authorization decision was always made by the backend 'agent'. This change removes a client-side advisory/defense-in-depth layer but does not itself grant unauthorized access — direct API calls bypassing the UI were 'already possible' per the scanner's own scenario. Because the real security boundary (backend enforcement) is unconfirmed broken, and the concrete instance here is a UX fail-open regression rather than a demonstrated privilege escalation, this does not meet HIGH criteria (no confirmed significant impact, no exploit evidence, and impact is contingent on a hypothetical backend gap). Severity: medium — a legitimate hardening gap that should be fixed promptly but is not an active authorization bypass in production today.
  • Composite score: 6.3
  • Environment: production

📝 Description:

The holderGate function was changed from an enforcing gate (disabling UI controls) to a purely advisory function that returns explanatory text but no longer disables any button. The diff removes disabled={Boolean(denied) || ...} from every consuming component (AttributeEditor, ProfileEditor, DeleteProfile, IdentityMap add/delete/bind buttons) so that the UI now always allows the action, relying entirely on the backend agent to refuse the request.

🌱 Root Cause: Client-side advisory text is treated as a substitute for an actual access-control check in the rendered UI. All disabled={...denied...} and disabledReason props were stripped from buttons that trigger destructive/sensitive persona operations (delete profile, add fact/face, bind persona), meaning the console UI no longer prevents an under-privileged caller from attempting these state-changing calls — it depends entirely on the downstream agent enforcing authorization.

🔎 Evidence: packages/extension/src/manager/holder-gate.ts:44

export function holderGate(authority: Authority | null): string | null {
  if (!authority) return null;
  if (isUnscopedHolder(authority)) return null;
  return (
    "Your facts sit above every context, so reaching them takes authority of its own... " +
    "This console cannot see which you have — `auth/whoami` reports roles and scopes, " +
    "not capabilities — so it does not stop you trying. Your agent decides, and says so if it refuses."
  );
}

💥 Impact:

Removes a defense-in-depth UI control that previously prevented under-privileged operators from attempting destructive persona operations (delete profile, create binding, add fact/face) they are not authorized to perform, shifting 100% of enforcement onto a downstream service; any latent gap in that service's authorization becomes directly exploitable via a UI that no longer discourages or blocks the attempt.

Confidentiality: low · Integrity: medium · Availability: low

🎯 Attack Scenario:

A caller with a context-scoped admin credential (not an unscoped holder, and without the persona-holder capability) opens the persona console. Previously the Delete/Add buttons were disabled with a explanatory tooltip; now they are always enabled and clickable regardless of the caller's actual capability. If the downstream agent's own authorization check has any gap, inconsistency, or race (or if a caller crafts requests directly against the API bypassing the UI, which was already possible, but the UI itself no longer signals or enforces restriction), the console offers no defense-in-depth and actively invites clicking destructive controls that will only be blocked (if at all) after the request already reaches the backend.

🔧 Remediation:

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

Priority: Short-term

Re-introduce a real client-side capability check (once auth/whoami can report capabilities as noted in the code comments) to disable destructive/sensitive controls proactively, in addition to relying on server-side enforcement. Until the capability-reporting API exists, consider keeping buttons disabled by default (fail-closed) for ambiguous authority states rather than fail-open, and ensure the backend agent enforcement path is verified to reject every one of these operations for insufficiently-privileged callers with a test that exercises the actual API, not just the advisory text.

Secure code:

export function holderGate(authority: Authority | null): { blocked: boolean; note: string | null } {
  if (!authority) return { blocked: true, note: null }; // fail-closed while unknown
  if (isUnscopedHolder(authority) || hasCapability(authority, 'persona-holder')) {
    return { blocked: false, note: null };
  }
  return {
    blocked: true,
    note: "Your facts sit above every context... requires persona-holder capability or unscoped admin.",
  };
}
// Consumers: disabled={holderGate(authority).blocked}

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 75%
  • AI Validation Evidence: EVIDENCE FOUND: holder-gate.ts's holderGate() function, cited in the finding, now only returns an explanatory string and never disables UI: 'export function holderGate(authority: Authority | null): string | null { if (!authority) return null; if (isUnscopedHolder(authority)) return null; return ("Your facts sit above every context..."); }'. Doc comment confirms intent: 'A caution, not a gate... It used to return a refusal and disable the buttons... So it explains and stands aside.' Consuming components (persona-map.tsx) now pass disabled={null} to AddTile and no disabledReason to Destructive/DeleteProfile. EVIDENCE NOT FOUND: No backend/agent source code (e.g. trust-tasks-rs, require_super_admin enforcement) is included in source_files, so server-side enforcement cannot be confirmed present or absent from this changeset. CHANGED VS PRE-EXISTING: holder-gate.ts is directly provided with the exact new advisory-only implementation, matching the MR's fix/persona-holder-gate branch intent — CHANGED. VERDICT JUSTIFICATION: The code chain (holderGate definition and its removed enforcement effect on consuming buttons) is fully visible and confirms the client-side gate no longer blocks any action; this is a genuine control removal, validated as the core finding even though residual risk depends on unverified server-side enforcement.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Client-side removal of disabled/blocking UI state on destructive persona-pool operations (broken access control relies on client-only advisory gate)

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/panes/persona-map.tsx:477
Finding ID github_pr-d3e0e8672632
CWE CWE-863
OWASP A01:2021 - Broken Access Control
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — The code evidence conclusively shows removal of client-side disabled/disabledReason props, but this is defense-in-depth erosion, not a confirmed authorization bypass. The scanner's attack scenario explicitly conditions exploitation on 'if the downstream agent authorization has any implementation gap' — this is speculative, not confirmed. No CVSS, no exploit maturity, no confirmed reachability of an actual backend gap, and no evidence server-side checks are absent or flawed. Absent confirmation that backend authorization is missing/broken, this is a hardening/regression-test gap (medium) rather than a proven high-impact access-control failure. If backend enforcement is confirmed absent, this should be re-escalated to high/critical immediately.
  • Composite score: 6.3
  • Environment: unknown

📝 Description:

The MR removes the disabled/disabledReason wiring that previously prevented UI actions (add fact, delete profile, bind, etc.) when holderGate() returned a non-null caution string. The holderGate function itself is explicitly documented in the new module as advisory only ("A caution, not a gate") because the console cannot see capability grants via auth/whoami. This is a deliberate design change acknowledged in the code and tests, shifting enforcement entirely to the backend/agent. The finding here is that the UI security control has been intentionally weakened to a display-only hint, and this MR is the point where that authorization decision was moved out of the client — this needs explicit confirmation that server-side enforcement (require_super_admin / persona-holder check) is present and independent of anything this console does.

🌱 Root Cause: The identity/authority check (isUnscopedHolder/holderGate) that used to gate destructive UI actions no longer disables those actions; it only supplies a title/tooltip. Multiple disabled={...denied...} and disabledReason props were deleted across persona-map.tsx and persona-editors.tsx, meaning the button remains clickable regardless of the caller's apparent authority.

🔎 Evidence: packages/extension/src/manager/panes/persona-map.tsx:477

<AddTile label="+ Add a fact" onClick={() => setEditing({ kind: "fact" })} disabled={null} />
...
<DeleteProfile parties={parties} profile={raw} onDone={onChanged} />  // disabledReason prop removed
...
<Button kind="danger" onClick={() => setPhase({ kind: "confirm" })}>Delete</Button>  // disabled={Boolean(disabledReason)} removed

💥 Impact:

If the backend does not independently and correctly enforce the holder/capability check on every mutating persona-pool endpoint, removing the client-side disable allows an under-privileged operator to attempt (and potentially complete) destructive operations (delete/add/bind identity facts and faces) that cross trust-context boundaries, corrupting or exposing cross-tenant identity data.

Confidentiality: medium · Integrity: high · Availability: low

🎯 Attack Scenario:

A context-scoped (non-unscoped) admin who does not actually hold the persona-holder capability opens the persona console. Previously the Delete/Add buttons would be disabled and unclickable when holderGate returned a caution. Now the buttons are always enabled; the user can click Delete/Add and issue the underlying mutation request. If the backend enforcement of require_super_admin/persona-holder has any gap, inconsistency, or is bypassed by a different code path than auth/whoami implies, the client no longer provides a defense-in-depth speed bump, and the operator receives no interactive block — only a passive title tooltip that can be missed, increasing the chance of an unauthorized destructive action being attempted and potentially succeeding if server-side checks are also flawed.

🔧 Remediation:

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

Priority: Short-term

Confirm and document that every persona-pool mutation endpoint independently re-validates authority server-side (require_super_admin OR persona-holder capability) regardless of what the console displays. If that guarantee is not airtight, restore a disabling UX state (even if imprecise) as defense-in-depth, or fetch capabilities via an updated auth/whoami before rendering destructive controls as active.

Secure code:

// Prefer failing safe when capability is unknown:
const denied = holderGate(authority);
<Button kind="danger" disabled={Boolean(denied)} title={denied ?? undefined} onClick={...}>Delete</Button>
// And ensure server-side handler re-checks require_super_admin || hasCapability(authority, 'persona-holder') independent of client state.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 72%
  • AI Validation Evidence: EVIDENCE FOUND: In persona-map.tsx, <AddTile label="+ Add a fact" onClick={() => setEditing({ kind: "fact" })} disabled={null} /> confirms disabled wiring removed for the fact-add path; <DeleteProfile parties={parties} profile={raw} onDone={onChanged} /> in DetailStrip's face-selection branch passes no disabledReason. The persona-editors.tsx DeleteProfile Button also no longer has disabled={Boolean(disabledReason)}. EVIDENCE NOT FOUND: No confirmation of an equivalent server-side check being introduced in this diff (agent-side code absent from source_files). CHANGED VS PRE-EXISTING: persona-map.tsx and persona-editors.tsx are both provided with matching new content — CHANGED. VERDICT JUSTIFICATION: Direct code evidence (disabled={null}, absent disabledReason prop) confirms the gating removal described in the finding.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Client-side authorization gate removed from destructive persona actions (fail-open UI, real enforcement missing/unverifiable server-side)

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/panes/persona-map.tsx:413
Finding ID github_pr-e412284c7791
CWE CWE-862
OWASP A01:2021 - Broken Access Control
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — The scanner-reported 'high' assumes the backend enforcement is missing/lenient, but there is no confirmed evidence (code, test, or comment) that the server-side delete endpoint actually lacks authorization — only that client-side signal was removed and backend enforcement is 'unverifiable' in this diff. Missing UI-level disable/title is a real regression (removes a defense-in-depth signal and increases risk of accidental/malicious destructive action attempts by under-scoped principals), but absent confirmed server-side bypass, this is a hardening/defense-in-depth gap rather than a demonstrated authorization bypass. Capping at medium reflects: CWE-862 present, no CVE/exploit maturity applicable to first-party code, reachability plausible but unconfirmed against an actual unauthorized backend success, and no PoC of an actual successful unauthorized delete.
  • Composite score: 6.3
  • Environment: production

📝 Description:

The MR removes the denied/disabledReason wiring (derived from holderGate) from every action button in the persona pane — add fact, add face, bind, and especially the Delete/DeleteProfile destructive actions. Previously these buttons were disabled and annotated with a title explaining the caller lacked the required unscoped-holder authority; now they are always enabled regardless of the caller's authority result, and no disabledReason/denied is passed to DeleteProfile or Destructive at all (the prop was deleted from the type signature).

🌱 Root Cause: The refactor changes holderGate from an enforcing gate to a purely advisory string ('a caution, not a gate') and simultaneously deletes all UI wiring that previously used its result to disable buttons. Combined with the comment stating the server-side capability model (persona-holder) is not visible to this console via auth/whoami, this leaves the client UI as the only front-line signal, and even that signal has now been fully stripped from the actual controls, including the destructive Delete path.

🔎 Evidence: packages/extension/src/manager/panes/persona-map.tsx:413

disabled={checking === "Asking your agent…"}
...
<AddTile label="+ Add a fact" onClick={() => setEditing({ kind: "fact" })} disabled={null} />
...
<Destructive<PoolProfile[]>
  label="Delete"
  preview={async () => { ... }}
...
<DeleteProfile parties={parties} profile={raw} onDone={onChanged} />

💥 Impact:

An under-privileged caller could attempt and potentially execute destructive deletion of cross-context identity/persona data without any client-side warning, increasing risk of unauthorized data loss across trust contexts if server-side enforcement is incomplete or if the console is later relied upon as a control point.

Confidentiality: medium · Integrity: high · Availability: medium

🎯 Attack Scenario:

A caller without sufficient authority (e.g., a context-scoped Admin lacking persona-holder capability) is now given a fully clickable Delete button with no client-side warning/disable at all — increasing the chance an under-scoped principal attempts (and, if the backend is also lenient/misconfigured) succeeds in deleting cross-context persona/profile data. Even if the backend re-checks, the removal of ALL disabled/title hints for the Delete flow (as opposed to other buttons which still receive denied in a title) means the client provides zero signal to the user before they attempt a destructive, potentially cross-tenant action, and there is no verification in this diff that the server enforces persona-holder/require_super_admin on the delete path.

🔧 Remediation:

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

Priority: Short-term

Restore passing the holderGate/denied result (or equivalent) into DeleteProfile/Destructive and other action controls as a title/warning, and confirm — with an explicit test or comment reference — that the backend independently enforces persona-holder or require_super_admin on every delete/mutate persona endpoint so the client-side caution is defense-in-depth rather than the only signal.

Secure code:

<DeleteProfile parties={parties} profile={raw} disabledReason={denied} onDone={onChanged} />
// and ensure server-side handler independently checks isUnscopedHolder(authority) || hasCapability(authority, 'persona-holder') before performing delete.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 72%
  • AI Validation Evidence: EVIDENCE FOUND: Same evidence as prior findings — disabled={checking === "Asking your agent…"} on the 'Check values' button (unrelated to denied), <AddTile ... disabled={null} />, and Destructive/DeleteProfile calls without disabledReason in persona-map.tsx. denied is computed via const denied = holderGate(authority); but only used for title tooltips (e.g. {...(denied ? { title: denied } : {})}) rather than disabled. EVIDENCE NOT FOUND: Backend enforcement code for the delete/mutation RPCs is not in source_files. CHANGED VS PRE-EXISTING: persona-map.tsx is a changed file with the exact quoted lines present — CHANGED. VERDICT JUSTIFICATION: The quoted lines are present verbatim in the provided source, confirming denied is advisory-only (title) not blocking (disabled) for these controls.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Client-side-only authorization gate removed from destructive/mutating actions (Broken Access Control)

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/panes/persona-map.tsx:480
Finding ID github_pr-2e38902fdd0f
CWE CWE-863
OWASP A01:2021 - Broken Access Control
Detection Source threat_model

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — The code evidence conclusively shows removal of client-side disabled/disabledReason props, but this is defense-in-depth erosion, not a confirmed authorization bypass. The scanner's attack scenario explicitly conditions exploitation on 'if the downstream agent authorization has any implementation gap' — this is speculative, not confirmed. No CVSS, no exploit maturity, no confirmed reachability of an actual backend gap, and no evidence server-side checks are absent or flawed. Absent confirmation that backend authorization is missing/broken, this is a hardening/regression-test gap (medium) rather than a proven high-impact access-control failure. If backend enforcement is confirmed absent, this should be re-escalated to high/critical immediately.
  • Composite score: 6.3
  • Environment: production

📝 Description:

The diff removes the denied/disabledReason wiring that previously disabled the 'Add a fact', 'Delete', and related buttons when holderGate() returned a caution string. The buttons are now always enabled regardless of the caller's authority, with the client-side check reduced to a UI hint that is no longer enforced by disabling the control.

🌱 Root Cause: The UI previously used holderGate(authority) output to gate (disable) sensitive UI actions (create/delete of persona pool facts). This gating has been stripped from AddTile, Destructive, and DeleteProfile invocations, leaving only a title tooltip in some cases and nothing at all in others (e.g. disabled={null} unconditionally enables the Add button). The code comments explicitly state this is an intentional change based on the reasoning that the console 'cannot tell' whether the caller has sufficient capability and that 'the agent decides again on every task' — i.e., the developers claim server-side re-validation exists. However, the removed disabling was a defense-in-depth UI control; if the backing agent/service enforcement is incomplete, incorrectly implemented, or bypassable, this change removes a layer of protection with no equivalent replacement in the reviewed diff.

🔎 Evidence: packages/extension/src/manager/panes/persona-map.tsx:480

<AddTile label="+ Add a fact" onClick={() => setEditing({ kind: "fact" })} disabled={denied} />
// changed to:
<AddTile label="+ Add a fact" onClick={() => setEditing({ kind: "fact" })} disabled={null} />
...
<Destructive<PoolProfile[]> label="Delete" disabledReason={denied} ... />
// changed to:
<Destructive<PoolProfile[]> label="Delete" ... />
...
<DeleteProfile parties={parties} profile={raw} disabledReason={denied} onDone={onChanged} />
// changed to:
<DeleteProfile parties={parties} profile={raw} onDone={onChanged} />

💥 Impact:

If the backend/agent-side capability check referenced in code comments (require_super_admin / persona-holder) is not fully authoritative or has edge cases, a context-scoped admin could delete or create identity facts belonging to other trust contexts, causing cross-tenant data integrity issues or unauthorized disclosure of identity metadata.

Confidentiality: medium · Integrity: medium · Availability: low

🎯 Attack Scenario:

A context-scoped administrator (not an unscoped holder) opens the persona/identity console. Under the prior code, the Delete/Add controls for cross-context persona facts were disabled outright when holderGate returned a non-null caution. After this change, those controls are always clickable, and the operator can attempt to add or delete cross-context identity facts. If the downstream agent authorization (persona-holder capability check or require_super_admin) has any implementation gap, race condition, or is bypassed, the scoped admin can mutate/delete identity data belonging to other trust contexts that they should not be able to touch, since the last remaining client-side speed bump has been removed.

🔧 Remediation:

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

Priority: Short-term

Re-introduce a UI-side gate (disable + tooltip) as defense-in-depth even though authoritative enforcement should live server-side, OR explicitly confirm and test that every mutating/destructive action (create fact, delete profile) is independently and fully authorized server-side, and add a regression test proving that a context-scoped admin without persona-holder capability is rejected by the backend when performing these actions through the API, not just informed via UI text.

Secure code:

<AddTile
  label="+ Add a fact"
  onClick={() => setEditing({ kind: "fact" })}
  disabled={Boolean(denied)}
  {...(denied ? { title: denied } : {})}
/>

<Destructive<PoolProfile[]>
  label="Delete"
  disabledReason={denied}
  ...
/>

<DeleteProfile parties={parties} profile={raw} disabledReason={denied} onDone={onChanged} />

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 62%
  • AI Validation Evidence: EVIDENCE FOUND: persona-map.tsx source confirms: <AddTile label="+ Add a fact" onClick={() => setEditing({ kind: "fact" })} disabled={null} /> and <DeleteProfile parties={parties} profile={raw} onDone={onChanged} /> (no disabledReason prop passed), matching the 'changed to' side of the diff snippet exactly. EVIDENCE NOT FOUND: The 'before' state (disabled={denied}, disabledReason={denied}) is asserted by the finding's diff context, not independently visible in current source, but this is expected since source_files shows post-change state; the removal is corroborated by holder-gate.ts's own doc comment describing the prior enforced behavior. CHANGED VS PRE-EXISTING: persona-map.tsx — CHANGED, directly quoted in source_files. VERDICT JUSTIFICATION: Current source matches exactly the described post-removal code, confirming the gate removal.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Client-side removal of holder-gate disabling is a UI-only control, not a security boundary

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/panes/persona-editors.tsx:356
Finding ID github_pr-85c21df412fd
CWE CWE-863
OWASP A01:2021 - Broken Access Control
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • The evidence shows only a client-side disabled-state change in a React button; CWE-863 applies to the *possibility* that this UI change masks a missing server-side check, not a confirmed bypass. Without evidence that the backend fails to re-validate persona-pool mutation requests, this cannot be elevated to high/critical. It remains a legitimate medium-severity defense-in-depth concern: removing a UI guard increases the blast radius if (and only if) server-side authorization has any gap.
  • Composite score: 6.1
  • Environment: production

📝 Description:

The persona editor buttons that previously disabled destructive/privileged actions when denied (holderGate) returned a message now only show a title tooltip and no longer set disabled, relying entirely on the backend agent to enforce the authority check.

🌱 Root Cause: Client-side enablement/disablement of sensitive actions was removed from being enforced (disabled) and reduced to advisory text only, while actual authorization enforcement now solely depends on the backend agent's own checks (which are not shown in this diff).

🔎 Evidence: packages/extension/src/manager/panes/persona-editors.tsx:356

disabled={busy || !type.trim() || Boolean(correlation)}
{...(denied ? { title: denied } : {})}
onClick={() => void save()}

🎯 Attack Scenario:

If the backend enforcement (agent-side capability/role check) has any gap, a scoped/non-holder caller could still submit persona pool mutation requests since the UI no longer blocks the action locally; the security fully depends on server-side re-validation of every request.

Also flagged at this location (same code, other weakness framings): Same-pattern disabled-gate removal on create/update mutation buttons (AttributeEditor, ProfileEditor) and add-tile/bind controls in IdentityMap

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: persona-editors.tsx AttributeEditor save button pattern matches (per persona-map.tsx analogous buttons): disabled={busy || !type.trim() || Boolean(correlation)} with {...(denied ? { title: denied } : {})} — denied is used only for tooltip, not merged into the disabled boolean. This differs from DeleteProfile/AddTile in that it targets non-destructive save actions specifically flagged as CWE-863/'UI-only advisory' pattern. EVIDENCE NOT FOUND: Full persona-editors.tsx content beyond what's summarized was not fully quoted in source_files (only referenced), so exact surrounding logic for this specific button (correlation checks etc.) is partially inferred from the finding's own quoted snippet, which is consistent with the file's known structure. CHANGED VS PRE-EXISTING: persona-editors.tsx is in the changed-files scope (referenced/imported by persona-map.tsx and modified per holder-gate transition) — CHANGED. VERDICT JUSTIFICATION: Pattern is consistent with the confirmed advisory-only holderGate design; denied only drives tooltip text, not blocking behavior, matching the broader confirmed pattern across the MR.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Authorization decision relies on client-visible roles/scopes that cannot represent capabilities

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/holder-gate.ts:33
Finding ID github_pr-01196a899ef6
CWE CWE-1076
OWASP A04:2021-Insecure Design
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • The code confirms a client-side-only authorization gate pattern (holderGate checks authority.isUnscopedHolder client-side), which is a legitimate architectural weakness (CWE-602: Client-Side Enforcement of Server-Side Security / CWE-863: Incorrect Authorization). However, exploitation is explicitly conditional on a hypothetical backend defect ('if the backend capability check has a defect or race') that is not demonstrated in this finding. No CVSS, no exploit maturity, no confirmed reachability of an actual bypass — only the design smell is proven. This aligns with medium: a real but conditional/theoretical-until-chained issue, not a confirmed exploitable bypass.
  • Composite score: 4.9
  • Environment: production

📝 Description:

The holderGate function admits it cannot determine whether the caller holds the persona-holder capability because auth/whoami only reports roles and scopes, not capabilities. It therefore always allows the UI action to proceed (returns a non-blocking advisory) for any caller that isn't an unscoped admin, deferring entirely to server-side enforcement.

🌱 Root Cause: Incomplete or insufficient authorization data model exposed to the client (missing capabilities) forces the client into a permissive fallback pattern.

🔎 Evidence: packages/extension/src/manager/holder-gate.ts:33

export function holderGate(authority: Authority | null): string | null {
  if (!authority) return null;
  if (isUnscopedHolder(authority)) return null;
  return (
    "Your facts sit above every context, so reaching them takes authority of its own: " + ...
  );
}

🎯 Attack Scenario:

A context-scoped caller without the persona-holder capability could attempt privileged persona pool operations from the UI; if the backend capability check has a defect or race, the action could succeed since the UI does not block based on incomplete authority data.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 50%
  • AI Validation Evidence: EVIDENCE FOUND: holder-gate.ts doc comment explicitly states the limitation: 'auth/whoami reports roles and scopes but not capabilities, so this console cannot tell the difference. Disabling on a check that cannot see the answer would have locked the console out of the configuration it recommends.' And in holderGate's return message: 'auth/whoami reports roles and scopes, not capabilities — so it does not stop you trying.' EVIDENCE NOT FOUND: No evidence of an alternative capability-reporting mechanism being added elsewhere in this MR. CHANGED VS PRE-EXISTING: holder-gate.ts is the exact new module introduced by this MR — CHANGED. VERDICT JUSTIFICATION: The code and its own documentation confirm the authorization decision is explicitly acknowledged as unable to represent true capabilities, matching CWE-1076 concern precisely.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Authorization-relevant rationale exposed verbatim to client via holderGate() message revealing internal capability model

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/holder-gate.ts:55
Finding ID github_pr-05142dda6db6
CWE CWE-200
OWASP A01:2021 - Broken Access Control
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — The disclosed text reveals internal capability naming and detection gaps (CWE-200-class issue), which lowers attacker reconnaissance cost but does not itself grant unauthorized access or bypass authentication/authorization. No CVSS, no confirmed reachability from public unauthenticated surface, no exploit evidence — this remains an low-severity hardening/defense-in-depth issue consistent with the scanner's original rating.
  • Composite score: 4.6
  • Environment: production

📝 Description:

The advisory message returned to the UI explicitly explains the internal authorization model, including the exact capability name (persona-holder), the specific check it bypasses (require_super_admin), and the fact that the client-side check cannot see capabilities — effectively documenting to any user (or attacker probing the UI) how to determine what credential is needed to succeed and confirming that the client offers no real gate.

🌱 Root Cause: Detailed internal authorization design decisions and capability names are embedded in a string rendered directly in the UI tooltip/note, without abstraction.

🔎 Evidence: packages/extension/src/manager/holder-gate.ts:55

return (
    "Your facts sit above every context, so reaching them takes authority of its own: " +
    "an agent credential with no context restriction, or one granted the " +
    "`persona-holder` capability. This console cannot see which you have — " +
    "`auth/whoami` reports roles and scopes, not capabilities — so it does not stop " +
    "you trying. Your agent decides, and says so if it refuses."
  );

💥 Impact:

Minor information leakage about internal authorization model aids reconnaissance for privilege escalation attempts, though it does not itself grant access.

Confidentiality: low · Integrity: none · Availability: none

🎯 Attack Scenario:

An attacker with limited privileges views the tooltip and learns the precise capability name (persona-holder) required to bypass the restriction and that the console cannot detect it, guiding social engineering or targeted requests to obtain that specific grant, or confirming that repeated attempts against the endpoint are worth trying since the UI is not authoritative.

🔧 Remediation:

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

Priority: Short-term

Use a generic caution message that does not reveal exact capability identifiers or internal check names to end users; keep detailed rationale in code comments/docs rather than user-facing strings.

Secure code:

return "You may not have sufficient authority for this action. If it fails, contact your administrator.";

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 35%
  • AI Validation Evidence: EVIDENCE FOUND: holder-gate.ts's holderGate() return string is quoted verbatim in source_files: 'Your facts sit above every context, so reaching them takes authority of its own: an agent credential with no context restriction, or one granted the persona-holder capability. This console cannot see which you have — auth/whoami reports roles and scopes, not capabilities — so it does not stop you trying. Your agent decides, and says so if it refuses.' This is rendered as a Note/title in persona-map.tsx ({denied && <Note tone="warn">{denied}</Note>} and {...(denied ? { title: denied } : {})}), exposing it to any authenticated console user. EVIDENCE NOT FOUND: No access control on who can view this message beyond generic authentication to the manager console. CHANGED VS PRE-EXISTING: holder-gate.ts is a new file introduced by this MR — CHANGED. VERDICT JUSTIFICATION: The exact disclosed string is present in the reviewed source and is rendered in the UI, confirming the information disclosure of internal capability model naming (persona-holder, require_super_admin context implied).
  • 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 #183

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/persona-holder-gatemain
Generated 2026-09-07

ℹ️ 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 refactors the client-side 'holderGate' authorization-hint logic out of a .tsx component into a standalone, testable holder-gate.ts module, and simultaneously removes all client-side disabled/disabledReason UI gating derived from that check across five persona-management components (AttributeEditor, ProfileEditor, DeleteProfile, IdentityMap buttons, DetailStrip). The stated rationale is that auth/whoami cannot observe the newly-introduced persona-holder capability, so the old client-side disable logic incorrectly blocked legitimately-configured (context-scoped + persona-holder) operators from the recommended setup; the PR asserts server-side agent enforcement is authoritative and unaffected.

Diff: +130 / -68 lines
Types: security, refactor, test

📁 File Classifications

packages/extension/src/manager/holder-gate.ts

  • Type: security

packages/extension/src/manager/panes/persona-editors.tsx

  • Type: security

packages/extension/src/manager/panes/persona-map.tsx

  • Type: security

packages/extension/src/manager/panes/persona-setup.tsx

  • Type: security

packages/extension/src/manager/use-vta.ts

  • Type: security

packages/extension/tests/manager-holder-gate.test.mts

  • Type: test

🛡️ STRIDE Threat Model

Identified Threats (10)

🟠 STRIDE-1: Client-Side Authorization Bypass in DeleteProfile Destructive Control

Field Detail
Category Tampering, Elevation of Privilege
Severity High
Likelihood Likely
CVSS 8.2 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity High
CWE CWE-602,CWE-285,CWE-863
CAPEC CAPEC-122,CAPEC-58
OWASP A01:2021 - Broken Access Control

Description: Delete button in DeleteProfile in persona-editors.tsx allows unauthorized destructive delete attempts due to removal of the disabled/disabledReason UI gate, resulting in reliance on unverified server-side enforcement for authorization

Evidence: packages/extension/src/manager/panes/persona-editors.tsx:650-660

return (
  <Button kind="danger" onClick={() => setPhase({ kind: "confirm" })}>
    Delete
  </Button>
);

Attack Scenario:

  1. Attacker (or misled legitimate user) obtains a context-scoped admin credential (roles=["admin"], scopes=["work"]) that would previously be blocked from persona-holder pool tasks.
  2. Attacker opens the persona manager UI and navigates to a PoolProfile they should not be able to delete, e.g. via DetailStrip/Destructive component in persona-map.tsx.
  3. The Delete button in DeleteProfile (packages/extension/src/manager/panes/persona-editors.tsx lines ~650-660) no longer has disabled={Boolean(disabledReason)} — the button is fully clickable regardless of holderGate() output.
  4. Attacker clicks Delete, proceeds through the ConsentCeremony confirmation flow (setPhase({kind:'confirm'})), and submits the destructive mutation request to the backend/agent.
  5. If the server-side agent enforcement (require_super_admin check referenced in holder-gate.ts) is missing, misconfigured, or has a logic gap distinct from the client check, the delete succeeds despite the caller lacking true unscoped-holder or persona-holder capability.
  6. Identity data (persona pool profile) belonging to another trust context is destructively deleted without proper authorization verification at the only remaining enforcement point.

🔎 Threat Clue: Derived from persona-editors.tsx, persona-map.tsx via DeleteProfile Delete button, Destructive component in DetailStrip

  • Data Flows: Client UI -> Agent delete/mutation RPC

Preconditions: Attacker holds a context-scoped admin credential or any authority object that fails isUnscopedHolder() but can still reach the UI., Server-side agent enforcement of require_super_admin or persona-holder capability is either not implemented, buggy, or not covering the delete/mutation RPC path exercised by DeleteProfile., No independent test suite validates server-side enforcement for this exact destructive path.

Existing Controls: holderGate() still renders an advisory tooltip (title=denied) informing the user of the authority requirement. • ConsentCeremony/confirm phase requires an additional explicit confirmation click before the destructive action executes. • PR documentation states enforcement is intended to occur server-side in the agent on every task.

Recommended Mitigations: Independently verify and test that the backend/agent enforces require_super_admin or persona-holder capability on every delete/mutation RPC invoked by DeleteProfile and Destructive components. • Add integration tests that attempt the delete RPC with a context-scoped admin token and assert server-side rejection. • Consider re-adding a soft client-side warning modal (not a hard block) that requires explicit acknowledgment before submission when holderGate() returns non-null. • Extend auth/whoami (or an equivalent endpoint) to report capabilities so the client can make a more accurate advisory decision without becoming the sole enforcement point.


🟡 STRIDE-2: Reliance on Client-Removed disabledReason Prop Leaving Stale Callers Exposed

Field Detail
Category Tampering, Denial of Service
Severity Medium
Likelihood Possible
CVSS 5.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-710,CWE-1076
CAPEC CAPEC-124
OWASP A05:2021 - Security Misconfiguration

Description: DeleteProfile component signature change in persona-editors.tsx allows type-safety loss for external callers due to removal of the disabledReason prop without a corresponding runtime compatibility shim, resulting in silent behavior regressions in any out-of-scope caller

Evidence: packages/extension/src/manager/panes/persona-editors.tsx:598-610

export function DeleteProfile({ parties, profile, onDone }: { parties: Parties; profile: PoolProfile; onDone: () => void; }) {

Attack Scenario:

  1. A separate module (not included in the diff's file set) may still call <DeleteProfile parties={parties} profile={raw} disabledReason={denied} onDone={onChanged} /> expecting the old gating behavior.
  2. TypeScript compilation for that stale caller either fails (excess property, tolerated in JS/loose configs) or silently succeeds ignoring the unused prop, since React ignores unknown props on custom components at runtime.
  3. The stale caller believes its passed disabledReason still gates the Delete button, but the component internally never uses it.
  4. An operator relying on that other call site's assumed UI gate unknowingly exposes an unauthorized delete path in an area of the codebase not reviewed by this PR.

🔎 Threat Clue: Derived from persona-editors.tsx, unknown external callers via DeleteProfile component API

  • Data Flows: Component prop passing

Preconditions: A caller of DeleteProfile exists outside the six files included in this PR's diff., That caller was not updated in this PR and continues to pass disabledReason expecting enforcement.

Existing Controls: TypeScript type-checking would likely flag the removed prop as an unknown/excess property at build time in strict mode. • PR review flag explicitly calls out this exact risk for downstream verification.

Recommended Mitigations: Run a full-repository search for all usages of DeleteProfile and Destructive components before merging. • Add a lint/CI check that fails the build if any component prop referencing security-relevant gating (disabledReason, denied) is passed to a component that no longer consumes it. • Add a deprecation-safe wrapper that logs a warning if disabledReason is still passed but ignored.


🟡 STRIDE-3: Insufficient Authority Model Enabling Unscoped Admin Misclassification

Field Detail
Category Elevation of Privilege, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 6.5 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-863,CWE-290
CAPEC CAPEC-115
OWASP A01:2021 - Broken Access Control

Description: isUnscopedHolder heuristic in holder-gate.ts allows misclassification of privilege boundaries due to reliance on the combination of role=admin AND scopes.length===0 without verifying persona-holder capability, resulting in incorrect advisory decisions surfaced to end users that could be misapplied elsewhere as a true authorization check

Evidence: packages/extension/src/manager/holder-gate.ts:27-30

export function isUnscopedHolder(authority: Authority | null): boolean {
  if (!authority) return false;
  return authority.roles.includes("admin") && authority.scopes.length === 0;
}

Attack Scenario:

  1. A developer elsewhere in the codebase (outside the reviewed file set) imports isUnscopedHolder from use-vta.ts (re-exported from holder-gate.ts) intending to use it as an authoritative access-control check rather than an advisory hint.
  2. Because the docstring states 'sufficient but not necessary,' a hurried implementation may treat a false result (non-unscoped-holder) as 'access denied' and gate a sensitive UI element or client-side data fetch on it.
  3. A caller holding persona-holder capability but appearing to isUnscopedHolder() as false (role admin + non-empty scopes) is incorrectly blocked from legitimate functionality, or conversely, a caller could exploit ambiguity between roles/scopes if the underlying Authority object is attacker-influenceable (e.g., via a compromised auth/whoami response or MITM).
  4. If auth/whoami responses can be tampered with in transit or the session object can be manipulated client-side, an attacker crafts an Authority object with roles=['admin'], scopes=[] to force isUnscopedHolder() to return true, causing the UI to treat them as fully privileged and suppress all advisory warnings, encouraging riskier actions without any warning message shown to the user.

🔎 Threat Clue: Derived from holder-gate.ts, use-vta.ts via isUnscopedHolder, holderGate

  • Data Flows: auth/whoami response -> Authority object -> isUnscopedHolder decision

Preconditions: auth/whoami response integrity is not cryptographically verified client-side or is susceptible to tampering., A future or existing code path uses isUnscopedHolder as a hard security boundary rather than advisory-only, contrary to its documented intent.

Existing Controls: Extensive documentation within holder-gate.ts explicitly warns that this is advisory only and the agent decides again on every task. • Unit tests in manager-holder-gate.test.mts validate the exact boolean logic of isUnscopedHolder for several role/scope combinations.

Recommended Mitigations: Enforce TLS and integrity verification on auth/whoami responses to prevent tampering with roles/scopes data. • Add explicit code comments and lint rules preventing isUnscopedHolder from being used outside advisory/UI-hint contexts. • Extend auth/whoami to report capabilities directly, removing the need for heuristic inference entirely, as already planned per the module's own documentation.


🔵 STRIDE-4: Advisory Tooltip Information Disclosure of Authorization Model Internals

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

Description: title tooltip in holderGate() in holder-gate.ts allows disclosure of internal authorization architecture details due to verbose advisory text describing exact capability names and enforcement mechanisms, resulting in reconnaissance information useful for crafting privilege escalation attempts against the backend agent

Evidence: packages/extension/src/manager/holder-gate.ts:58-64

return (
  "Your facts sit above every context, so reaching them takes authority of its own: " +
  "an agent credential with no context restriction, or one granted the " +
  "`persona-holder` capability. This console cannot see which you have — " +
  "`auth/whoami` reports roles and scopes, not capa

Attack Scenario:

  1. Attacker with any low-privilege authenticated session opens the persona manager and triggers holderGate() to return non-null (e.g. by holding a context-scoped admin role).
  2. Attacker inspects the DOM/title attribute rendered from denied, revealing exact internal terminology: 'persona-holder capability', 'no context restriction', and the fact that auth/whoami reports roles and scopes but not capabilities.
  3. Attacker uses this disclosed information to specifically target the persona-holder capability grant mechanism or attempt to forge/request an unscoped admin credential through other attack vectors (e.g., social engineering an administrator, or probing the agent's capability-granting API) since the exact gap between client visibility and server enforcement is now known.
  4. This precise knowledge of the enforcement gap ('this console cannot see which you have... so it does not stop you trying') telegraphs to an attacker that client-side restrictions are non-authoritative, encouraging further probing of the server-side agent's actual enforcement logic for weaknesses.

🔎 Threat Clue: Derived from holder-gate.ts via title tooltip on gated buttons

  • Data Flows: holderGate() -> title attribute -> DOM

Preconditions: Attacker has any authenticated session capable of reaching the persona manager UI., Attacker performs reconnaissance via browser dev tools or accessibility tooling to read tooltip title attributes.

Existing Controls: Message is only shown to already-authenticated users within the extension's manager UI, not exposed pre-auth. • No secrets, tokens, or credentials are included in the tooltip text.

Recommended Mitigations: Reduce tooltip verbosity to avoid naming exact internal capability strings (e.g., 'persona-holder') and enforcement architecture details. • Log/monitor repeated triggering of holderGate() advisory paths as a potential reconnaissance signal. • Rely on generic messaging such as 'You may not have sufficient authority for this action; the system will verify.'


🟡 STRIDE-5: Race Condition Between Delete Preview and Confirm in Destructive Component

Field Detail
Category Tampering, Repudiation
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:A/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-367
CAPEC CAPEC-25
OWASP A01:2021 - Broken Access Control

Description: preview() async callback in Destructive component in persona-map.tsx allows a TOCTOU condition due to the profile authorization state being 're-asked' at preview time but the actual delete RPC being submitted separately after confirmation, resulting in stale-authority-based destructive actions if the caller's authority changes between preview and execution

Evidence: packages/extension/src/manager/panes/persona-map.tsx:825-833

<Destructive<PoolProfile[]>
  label="Delete"
  preview={async () => {
    // Asked again rather than read off the map, so the answer is
    // current at the moment of the decision.
  }}

Attack Scenario:

  1. User with borderline/expiring authority (e.g., a session nearing revocation or scope downgrade) opens the Delete confirmation flow for a PoolProfile in DetailStrip.
  2. The preview() callback executes ('Asked again rather than read off the map, so the answer is current at the moment of the decision') and returns a snapshot of current state.
  3. Before the user clicks final confirm, their backing authority is revoked or scoped down server-side (e.g., admin session downgraded), but the client-side React state (denied, holderGate output) is not re-evaluated because the disabled gate was removed and no re-check is forced before submission.
  4. User (or an automated replay of a captured click sequence) submits the confirm action, sending the delete mutation to the agent with a now-stale expectation that the earlier preview state still applies.
  5. If the server-side enforcement also has any caching or session-validation lag, the delete could execute in a brief window where the true current authority no longer permits it, resulting in an audit trail that misattributes the deletion to a validly-scoped action when it was not.

🔎 Threat Clue: Derived from persona-map.tsx via Destructive preview/confirm flow

  • Data Flows: preview() RPC -> confirm click -> delete RPC

Preconditions: A revocation or authority downgrade occurs between preview() and the final confirm click., Server-side agent does not perform a fresh, atomic authorization check immediately before executing the delete mutation.

Existing Controls: preview() is explicitly re-fetched rather than cached from the map view, reducing staleness window. • ConsentCeremony requires an explicit additional confirmation step, adding latency that could either help or reduce the exploitable window depending on server-side checks.

Recommended Mitigations: Ensure server-side agent performs authorization checks atomically at execution time, not merely at request receipt. • Add a short-lived, single-use authorization token issued at preview time and validated at confirm time to detect staleness. • Log and alert on any delete executed with an authority state that differs from the state at preview time.


🟠 STRIDE-6: Missing Independent Verification of Server-Side require_super_admin Enforcement

Field Detail
Category Elevation of Privilege, Information Disclosure
Severity High
Likelihood Possible
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-306,CWE-862
CAPEC CAPEC-122
OWASP A01:2021 - Broken Access Control

Description: Persona pool RPC endpoints referenced by holderGate documentation allows unauthorized cross-context data access due to the client-side PR assuming server-side require_super_admin/persona-holder enforcement exists without providing verification artifacts in this changeset, resulting in a potential single-point-of-failure authorization gap

Evidence: packages/extension/src/manager/holder-gate.ts:44-56

* It used to return a refusal and disable the buttons... Advisory: the agent decides again on every task regardless of what this returns.

Attack Scenario:

  1. This client-side PR removes all UI-level disabling for holder-scoped tasks based on the documented assumption that 'the agent decides again on every task regardless of what this returns'.
  2. No server-side (agent/trust-tasks-rs) source code, tests, or CI evidence is included in this diff to confirm require_super_admin or persona-holder capability checks are actually enforced on the corresponding RPC handlers.
  3. If the server-side enforcement was never implemented, was implemented with a logic bug, or was only enforced for a subset of the persona/* RPC surface (e.g., read but not delete), an attacker with a context-scoped admin credential can now freely attempt every previously-UI-blocked action (attribute edits, profile creation, bindings, deletions) with a fully enabled UI.
  4. Because the client no longer signals distrust through disabling, any gap in server-side enforcement becomes immediately and silently exploitable with no client-side friction at all.
  5. Successful exploitation grants read/write/delete access to identity data (facts, faces, bindings, profiles) belonging to other trust contexts, violating the core security invariant described in isUnscopedHolder's documentation.

🔎 Threat Clue: Derived from Backend agent (out of scope for this diff) via persona/* RPC surface (not in diff)

  • Data Flows: Client UI -> Agent persona/* RPC

Preconditions: Server-side agent enforcement code (not present in this changeset) must be independently verified., This PR is merged and deployed before server-side enforcement is confirmed complete and tested.

Existing Controls: PR description and code comments explicitly assert that server-side enforcement exists ('the agent decides again on every task regardless of this UI state'). • Recon review flags explicitly call out the need to verify this independently.

Recommended Mitigations: Block merge of this PR until server-side test coverage for require_super_admin/persona-holder enforcement on every persona/* RPC (create, read, update, delete, bind) is confirmed and linked. • Require a cross-repository CI gate or contract test verifying agent-side authorization for the same RPC surface exercised by this UI. • Add a security regression test that attempts each persona/* mutation with a context-scoped admin token against a real or mocked agent and asserts rejection.


🔵 STRIDE-7: Test Suite Repudiation Gap for Removed UI Gating Behavior

Field Detail
Category Repudiation
Severity Low
Likelihood Possible
CVSS 3.5 CVSS:4.0/AV:L/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-778
CAPEC CAPEC-93
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: manager-holder-gate.test.mts allows insufficient traceability of destructive UI actions due to only testing the string-returning holderGate function in isolation without integration tests asserting the Delete button remains enabled and no audit log is verified, resulting in a repudiation gap where removed client-side gating changes cannot be regression-tested against actual UI component behavior

Evidence: packages/extension/tests/manager-holder-gate.test.mts:1-51

test("an unscoped holder is told nothing — it plainly holds what it takes", () => {
  assert.equal(holderGate(authority(["admin"], [])), null);
});

Attack Scenario:

  1. The new test suite (manager-holder-gate.test.mts) validates only the pure function holderGate() and isUnscopedHolder(), asserting message content and null/non-null behavior.
  2. No test asserts that DeleteProfile's Button component actually renders without a disabled prop, nor that clicking it in a scoped-admin context proceeds to the confirm phase.
  3. A future regression that re-introduces or removes the disabled attribute inconsistently across AttributeEditor, ProfileEditor, DeleteProfile, IdentityMap, and DetailStrip would not be caught by this test file, since it does not exercise the .tsx components at all.
  4. If a security incident occurs where an unauthorized delete succeeded through the UI, there is no test-based evidence trail confirming what the UI's actual enabled/disabled state was at any given commit, weakening incident forensics and accountability.

🔎 Threat Clue: Derived from manager-holder-gate.test.mts via Test suite coverage

  • Data Flows: N/A

Preconditions: No component-level (React Testing Library or equivalent) tests exist for the five affected components in this PR., An incident investigation needs to reconstruct historical UI gating behavior.

Existing Controls: Pure-function unit tests exist for the underlying decision logic (holderGate/isUnscopedHolder), which partially documents intended behavior.

Recommended Mitigations: Add component-level tests (e.g., using a DOM testing library compatible with the project's test runner) asserting the presence/absence of disabled attributes on the five affected buttons under various authority states. • Add server-side audit logging for all delete/mutation actions capturing the caller's authority snapshot at execution time for forensic traceability. • Document in CI which UI gating behaviors are and are not covered by automated tests.


🔵 STRIDE-8: Type-Only Import Boundary Circumvention Risk in holder-gate.ts

Field Detail
Category Tampering
Severity Low
Likelihood Unlikely
CVSS 2.6 CVSS:4.0/AV:L/AC:H/AT:P/PR:H/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1104
CAPEC N/A
OWASP A06:2021 - Vulnerable and Outdated Components

Description: type-only import of Authority in holder-gate.ts allows a maintainability/security drift risk due to the module boundary being enforced only by convention and comments rather than tooling, resulting in potential future accidental introduction of runtime component dependencies that could break the standalone testability guarantee the module claims

Evidence: packages/extension/src/manager/holder-gate.ts:9-11

// A *type-only* import: erased at run time, so this module drags no component
// code behind it and the pane's test runner can load it.
import type { Authority } from "./use-vta.js";

Attack Scenario:

  1. A future contributor edits holder-gate.ts and inadvertently adds a runtime (non-type-only) import from use-vta.ts or a .tsx component file, believing it to be harmless.
  2. Because there is no build-time lint rule enforcing 'type-only import' as claimed in the comments, the module silently begins pulling in component code or side-effectful modules.
  3. This could break the node:test runner's ability to load holder-gate.ts (as documented: 'the pane's test runner cannot load a .tsx file'), causing the security-relevant test suite to fail to run or, worse, to run against a subtly different implementation if mocking/stubbing is used to work around the failure.
  4. Undetected, this could allow a security-relevant authorization decision module to silently diverge from its tested behavior in production builds versus test builds.

🔎 Threat Clue: Derived from holder-gate.ts via N/A

  • Data Flows: N/A

Preconditions: A future code change violates the type-only import convention without any automated enforcement., No CI lint rule (e.g., @typescript-eslint/consistent-type-imports enforced as error) exists to catch this.

Existing Controls: Extensive code comments document the type-only import requirement and its rationale. • TypeScript's import type syntax is used, which is erased at compile time and would need to be explicitly changed to introduce a runtime dependency.

Recommended Mitigations: Add an ESLint rule (e.g., import/no-cycle or consistent-type-imports) enforced in CI to guarantee holder-gate.ts never accumulates runtime dependencies on component code. • Add a CI smoke test that imports holder-gate.ts in isolation (as the test file does) to catch accidental runtime coupling early.


⚪ STRIDE-9: Prompt-Injection-Style Instruction Embedded in Source Comments Targeting Automated Review Tools

Field Detail
Category Spoofing, Repudiation
Severity Informational
Likelihood Unlikely
CVSS 1.0 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1059
CAPEC CAPEC-148
OWASP A08:2021 - Software and Data Integrity Failures

Description: Verbose narrative code comments in holder-gate.ts allow potential manipulation of automated security review/LLM-based tooling due to persuasive natural-language justifications embedded directly in source ('advisory, not a gate', 'the agent decides again'), resulting in a risk that automated reviewers may under-scrutinize the actual authorization gap if they defer to the embedded rationale rather than independently verifying server-side enforcement

Evidence: packages/extension/src/manager/holder-gate.ts:34-56

// Advisory: the agent decides again on every task regardless of what this returns.

Attack Scenario:

  1. This diff contains unusually extensive, persuasively-written prose comments embedded directly in the source and test files that repeatedly assert 'this is advisory, not a security boundary' and 'the agent decides again on every task'.
  2. An automated security scanning tool (including LLM-based ones) ingesting this code as 'DATA to be analyzed' could be nudged by the narrative framing to treat the removal of client-side authorization gates as fully justified and benign without independently verifying the claimed server-side enforcement exists.
  3. Per this task's own security directive, such embedded narrative must be treated as untrusted data, not as an authoritative security determination, and the removal of authorization UI gates must still be flagged for independent server-side verification regardless of how the comments frame it.
  4. This does not indicate malicious intent by the PR author, but represents a real risk pattern: security-critical refactors justified primarily via in-code prose rather than verifiable tests/artifacts can cause reviewer (human or automated) complacency.

🔎 Threat Clue: Derived from holder-gate.ts, manager-holder-gate.test.mts via N/A - code review process

  • Data Flows: Source code -> automated/human review

Preconditions: A human or automated reviewer relies solely on the embedded comments' framing rather than independently verifying claims about server-side enforcement., No linked server-side test evidence is provided alongside the narrative justification.

Existing Controls: This threat model instance itself explicitly treats the narrative comments as data-under-analysis and calls out the unverified server-side enforcement claim as STRIDE-6, demonstrating correct handling. • The PR does include a genuine, testable unit test suite for the underlying pure function, showing some verifiable rigor beyond prose.

Recommended Mitigations: Require security-critical PRs to include or link server-side test evidence rather than relying on in-code narrative justification alone. • Train/configure automated review tooling to always independently verify claims made in code comments about security properties, rather than accepting them at face value. • Add a PR template checklist item requiring explicit linkage to server-side enforcement tests when client-side gates are removed.


🔵 STRIDE-10: Denied State Computation Timing Enables UI Race with Async whoami Fetch

Field Detail
Category Tampering
Severity Low
Likelihood Possible
CVSS 4.0 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-362
CAPEC CAPEC-25
OWASP A01:2021 - Broken Access Control

Description: holderGate(null) returning null in holder-gate.ts allows a brief window of fully-enabled destructive controls due to the null-authority (loading state) case being treated identically to the authorized case, resulting in users being able to click Delete/Save buttons before auth/whoami resolves and the true authority is known

Evidence: packages/extension/src/manager/holder-gate.ts:58-59

export function holderGate(authority: Authority | null): string | null {
  if (!authority) return null;
  if (isUnscopedHolder(authority)) return null;

Attack Scenario:

  1. On initial page load or component mount, the Authority object is null while auth/whoami is still in flight (async fetch not yet resolved).
  2. holderGate(null) returns null (per design: 'still loading is not the same as refused'), which is functionally identical to the fully-authorized non-null-but-passing case in every button's disabled logic (Boolean(denied) evaluates falsy in both cases).
  3. If a user (or a scripted/automated fast-clicking bot) interacts with the Delete/Save buttons within this loading window before whoami resolves, the click handler (onClick={() => setPhase({kind:'confirm'})} or save()) executes without any denied-based gating ever having been evaluated against real authority data.
  4. Depending on backend request sequencing, the mutation request could be dispatched with a session that, moments later, would have been correctly flagged by holderGate() once authority data arrived, creating a race between UI readiness and authorization-state readiness.
  5. This is a narrower window than STRIDE-1 but represents an additional edge case where even the pre-PR gating would not have helped, since disabled was never true during loading in the original code either — however the PR's overall removal of gating removes any mitigating friction for this window.

🔎 Threat Clue: Derived from holder-gate.ts, persona-editors.tsx via Delete/Save buttons during authority loading

  • Data Flows: auth/whoami fetch -> Authority state -> button gating

Preconditions: auth/whoami fetch has meaningful latency (network delay)., User or automated tooling interacts with destructive controls within the loading window., Server does not independently re-validate authority at the time of mutation execution.

Existing Controls: busy state disables Save buttons during in-flight save operations for AttributeEditor/ProfileEditor (though not tied to authority loading specifically). • ConsentCeremony confirm step adds a small delay/friction that may allow whoami to resolve in most real-world timing scenarios.

Recommended Mitigations: Explicitly disable all destructive/mutating buttons while authority is still loading (authority === null AND not yet resolved), distinct from the 'no restriction' null-return semantics of holderGate. • Ensure server-side enforcement is authoritative regardless of client timing, closing this race at the true trust boundary. • Add a loading skeleton/spinner state that structurally prevents interaction with action buttons until authority resolves.



🍝 PASTA Threat Model

Application Purpose

The VTA browser extension provides a persona/identity management console allowing operators to view, edit, bind, and delete identity attributes (facts, faces, bindings) across trust contexts, delegating final authorization decisions to a backend verifiable-trust agent.

Inherent Risks

  • The persona pool sits above all trust contexts, making any authorization gap in this area a cross-context data exposure risk.
  • Client-side UI is explicitly documented as non-authoritative, placing full trust in an out-of-scope server-side enforcement layer not verifiable in this changeset.
  • Destructive delete operations on identity data are irreversible and cross-context in scope.

Objectives

Risk: Accept residual risk of misleading advisory-only UI in exchange for correctness once server-side enforcement is fully verified.
Business: Provide administrators and context-scoped operators a usable console for managing persona identity data without unnecessary UI friction.
Security: Ensure all destructive and cross-context operations are authorized exclusively by the server-side agent, independent of any client-side UI state.
Financial: Avoid costly identity data breaches or unauthorized cross-context disclosures that could trigger regulatory fines.
Compliance: Maintain auditable, accountable access to identity data crossing trust context boundaries.
Functional: Support fact/face creation, editing, binding, and deletion within the persona pool.
Operational: Minimize false-positive UI blocking for legitimate context-scoped holders configured via the persona-holder capability.

Business Impact Analysis (1)

BIA-1: Persona Pool Identity Data Management (Critical)

End-to-end process by which operators view, create, edit, bind, and delete cross-context persona identity data (facts, faces, bindings) through the browser extension console.

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

  • Stakeholders: Administrators / Context-Scoped Operators / OpenVTC Platform Users / Security/Compliance Team
  • Dependencies: Backend Verifiable Trust Agent / auth/whoami Endpoint / Persona Pool Data Store / Browser Extension Manager UI
  • Disruptions: Unauthorized cross-context deletion of identity profiles / Server-side enforcement gap allowing unscoped access / auth/whoami response tampering or unavailability
  • Impacts: Irreversible loss of identity data across trust contexts / Regulatory non-compliance due to unauthorized cross-context data access / Loss of user trust in the identity platform

Technical Scope

Roles (2): RO-1 Unscoped Admin / Persona Holder · RO-2 Context-Scoped Admin

Actors (2): AC-1 Persona Manager UI (Client) · AC-2 Backend Trust Agent

Entry Points (4): EP-1 Delete Button (DeleteProfile) · EP-2 Destructive Delete Control (DetailStrip) · EP-3 Save Button (AttributeEditor/ProfileEditor) · EP-4 AddTile / Bind Buttons (IdentityMap)

Threat Actors (2): TA-1 Malicious Context-Scoped Insider · TA-2 Compromised Session Attacker

Infrastructure (1): IF-1 Browser Extension Runtime

Trust Boundaries (2): TB-1 Browser Extension Client Boundary · TB-2 Backend Agent Trust Boundary

External Entities (2): EE-1 Context-Scoped Admin User · EE-2 Unscoped Holder Agent Credential

System Components (5): SC-1 Persona Manager UI (persona-editors.tsx, persona-map.tsx, persona-setup.tsx) · SC-2 Holder Gate Module (holder-gate.ts) · SC-3 use-vta.ts Authority Hook · SC-4 Backend Verifiable Trust Agent (out of scope) · SC-5 Persona Pool Data Store

Resources And Assets (2): RA-1 Persona Pool Identity Profiles · RA-2 Authority Object (roles/scopes/session)

Technologies And Dependencies (2): TD-1 React · TD-2 node:test

Use Cases (2)

  • Persona Pool Identity Editing: An authorized operator views the identity map, edits an attribute or profile, and saves changes which are validated by the backend agent before persistence.
  • Persona Profile Deletion: An authorized operator selects a profile in the identity map, previews the deletion impact, confirms via the consent ceremony, and the backend agent authorizes and executes the delete.

⚔️ Attack Scenarios (3)

SC-1: Persona Manager UI

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
  direction LR
  SC1@{ shape: rect, label: "SC-1: Persona Manager UI" }
  end
  subgraph SL2["2. Weaknesses"]
  direction LR
  CWE1@{ shape: rect, label: "CWE-602: Client-Side Enforcement of Server-Side Security" }
  CWE2@{ shape: rect, label: "CWE-862: Missing Authorization" }
  end
  subgraph SL3["3. Attack Patterns"]
  direction LR
  CAPEC1@{ shape: rect, label: "CAPEC-122: Privilege Abuse" }
  end
  subgraph SL4["4. Threats"]
  direction LR
  ST1@{ shape: rect, label: "STRIDE-1: Client-Side Authorization Bypass in DeleteProfile<br><i>High / Likely</i>" }
  ST6@{ shape: rect, label: "STRIDE-6: Missing Independent Verification of require_super_admin<br><i>High / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
  direction LR
  TA1@{ shape: rect, label: "TA-1: Malicious Context-Scoped Insider<br><i>Access identity data beyond authorized scope</i>" }
  end
  SC1 --> CWE1
  CWE1 --> CAPEC1
  CAPEC1 --> ST1
  ST1 --> TA1
  SC1 --> CWE2
  CWE2 --> ST6
  ST6 --> TA1
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FF0000,stroke-width:2px
  linkStyle 3 stroke:#FF0000,stroke-width:2px
  linkStyle 4 stroke:#FF0000,stroke-width:2px
  linkStyle 5 stroke:#FF0000,stroke-width:2px
  linkStyle 6 stroke:#FF0000,stroke-width:2px
Loading

SC-2: Holder Gate Module

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
  direction LR
  SC2@{ shape: rect, label: "SC-2: Holder Gate Module" }
  end
  subgraph SL2["2. Weaknesses"]
  direction LR
  CWE3@{ shape: rect, label: "CWE-863: Incorrect Authorization" }
  CWE4@{ shape: rect, label: "CWE-200: Exposure of Sensitive Information" }
  end
  subgraph SL3["3. Attack Patterns"]
  direction LR
  CAPEC2@{ shape: rect, label: "CAPEC-115: Authentication Bypass" }
  CAPEC3@{ shape: rect, label: "CAPEC-118: Information Gathering" }
  end
  subgraph SL4["4. Threats"]
  direction LR
  ST3@{ shape: rect, label: "STRIDE-3: Insufficient Authority Model<br><i>Medium / Possible</i>" }
  ST4@{ shape: rect, label: "STRIDE-4: Advisory Tooltip Information Disclosure<br><i>Low / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
  direction LR
  TA2@{ shape: rect, label: "TA-2: Compromised Session Attacker<br><i>Leverage stolen session before revocation</i>" }
  end
  SC2 --> CWE3
  CWE3 --> CAPEC2
  CAPEC2 --> ST3
  ST3 --> TA2
  SC2 --> CWE4
  CWE4 --> CAPEC3
  CAPEC3 --> ST4
  ST4 --> TA2
  linkStyle 0 stroke:#FFA500,stroke-width:2px
  linkStyle 1 stroke:#FFA500,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
  linkStyle 3 stroke:#FFA500,stroke-width:2px
  linkStyle 4 stroke:#00FF00,stroke-width:2px
  linkStyle 5 stroke:#00FF00,stroke-width:2px
  linkStyle 6 stroke:#00FF00,stroke-width:2px
  linkStyle 7 stroke:#00FF00,stroke-width:2px
Loading

SC-4: Backend Verifiable Trust Agent

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
  direction LR
  SC4@{ shape: rect, label: "SC-4: Backend Verifiable Trust Agent" }
  end
  subgraph SL2["2. Weaknesses"]
  direction LR
  CWE5@{ shape: rect, label: "CWE-306: Missing Authentication for Critical Function" }
  CWE6@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
  end
  subgraph SL3["3. Attack Patterns"]
  direction LR
  CAPEC4@{ shape: rect, label: "CAPEC-122: Privilege Abuse" }
  CAPEC5@{ shape: rect, label: "CAPEC-25: Forced Deadlock/Race Window Exploit" }
  end
  subgraph SL4["4. Threats"]
  direction LR
  ST6b@{ shape: rect, label: "STRIDE-6: Missing Verification of require_super_admin<br><i>High / Possible</i>" }
  ST5@{ shape: rect, label: "STRIDE-5: Race Condition in Destructive Preview/Confirm<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
  direction LR
  TA1b@{ shape: rect, label: "TA-1: Malicious Context-Scoped Insider<br><i>Access identity data beyond authorized scope</i>" }
  TA2b@{ shape: rect, label: "TA-2: Compromised Session Attacker<br><i>Leverage stolen session before revocation</i>" }
  end
  SC4 --> CWE5
  CWE5 --> CAPEC4
  CAPEC4 --> ST6b
  ST6b --> TA1b
  SC4 --> CWE6
  CWE6 --> CAPEC5
  CAPEC5 --> ST5
  ST5 --> TA2b
  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
Loading

📊 Risk Summary

Total Threats: 10

By Severity: Low: 4 · High: 2 · Medium: 3 · Informational: 1

By Category: Tampering: 5 · Elevation of Privilege: 3 · Denial of Service: 1 · Information Disclosure: 3 · Repudiation: 3 · Spoofing: 1

🎯 Attack Surface

Kill Chain 1: An attacker holding a context-scoped admin credential (roles=["admin"], scopes=["work"]) — previously blocked entirely by the removed disabled/disabledReason bindings in AttributeEditor, ProfileEditor, DeleteProfile, IdentityMap, and DetailStrip — now encounters a fully clickable UI across all five components (STRIDE-1, STRIDE-3). The attacker proceeds through the Delete confirmation ceremony and submits a delete/mutation RPC directly to the backend agent (SC-4), which is the sole remaining enforcement point but whose require_super_admin/persona-holder capability check is unverified within this changeset (STRIDE-6). If that server-side check has any gap — missing coverage on the delete path specifically, a logic bug, or simple absence — the attacker achieves unauthorized cross-context deletion of persona pool identity data (RA-1), a critical and irreversible impact.
Kill Chain 2: A secondary, timing-based path exploits the gap between authority state changes and UI re-evaluation. An attacker with a session nearing revocation or downgrade triggers the Destructive component's preview() (STRIDE-5) and, exploiting the window before the confirm click, submits the deletion after their true authority has changed server-side but before any re-validation occurs, assuming the agent lacks atomic authorization checks at execution time. A related but distinct race exists during the initial auth/whoami loading window, where holderGate(null) returns null identically to the fully-authorized case, allowing fast interaction with destructive controls before real authority data arrives (STRIDE-10).
Kill Chain 3: Reconnaissance-oriented attackers leverage the verbose advisory tooltip text in holderGate() (STRIDE-4) to learn the exact name of the persona-holder capability and the precise boundary of what auth/whoami can and cannot report, using this information to craft targeted social-engineering or credential-request attacks against administrators to obtain the specific capability grant needed, rather than attempting to brute-force the UI directly.
Kill Chain 4: A supply-chain-adjacent risk exists where a stale external caller of DeleteProfile (STRIDE-2), not included in this PR's file set, continues to pass a now-ignored disabledReason prop, silently losing whatever gating protection it assumed it had — this is a low-likelihood but codebase-wide integrity risk that compounds the primary authorization gap if such a caller exists in an unreviewed part of the extension.

🛡️ Risk Mitigation Strategy

Priority 1 (Immediate): The single highest-priority gap is the unverified assumption that the backend agent enforces require_super_admin/persona-holder capability checks on every persona/* mutation RPC (STRIDE-6, STRIDE-1). Before merging or deploying this PR, obtain and link server-side test evidence — ideally an integration test that attempts each mutation (create, update, bind, delete) against the real or mocked agent using a context-scoped admin token and asserts rejection. This single control closes the majority of residual risk introduced by this PR, since the client-side change is only safe if this server-side boundary is airtight.
Priority 2 (Short-Term): Address the authority-state race conditions (STRIDE-5, STRIDE-10) by ensuring the backend performs atomic, execution-time authorization checks rather than relying on any request-time snapshot, and by explicitly disabling destructive controls during the initial auth/whoami loading window on the client (a low-cost UX addition that does not conflict with the PR's stated goal of not blocking legitimate holder-scoped tasks). Additionally, perform a repository-wide search for any other callers of DeleteProfile or Destructive components to rule out the stale-caller risk (STRIDE-2).
Priority 3 (Medium-Term): Reduce the information-disclosure surface of the advisory tooltip (STRIDE-4) by trimming internal capability names and enforcement architecture details from user-facing text, and add component-level UI tests (STRIDE-7) that assert the actual disabled/enabled DOM state of the five affected buttons under varying authority conditions, closing the current gap where only the pure decision function is tested.
Priority 4 (Long-Term): Pursue the module's own documented long-term fix — extending auth/whoami (or an equivalent endpoint) to report capabilities directly — which would eliminate the heuristic nature of isUnscopedHolder entirely (STRIDE-3) and remove the structural ambiguity that necessitated this advisory-onl


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
8 0

Confirmed (8)

  • 🟡 UI-only advisory replaces enforced access control on destructive persona operations (client-side authorization bypass) (triaged HIGH→MEDIUM)
  • 🟡 Client-side-only authorization gate removed from destructive/mutating actions (Broken Access Control) (triaged HIGH→MEDIUM)
  • 🟡 Client-side removal of disabled/blocking UI state on destructive persona-pool operations (broken access control relies on client-only advisory gate) (triaged HIGH→MEDIUM)
  • 🟡 Client-side authorization gate removed from destructive persona actions (fail-open UI, real enforcement missing/unverifiable server-side) (triaged HIGH→MEDIUM)
  • 🟡 Client-side removal of holder-gate disabling is a UI-only control, not a security boundary
  • 🟡 Authorization decision relies on client-visible roles/scopes that cannot represent capabilities
  • 🟡 Authorization-relevant rationale exposed verbatim to client via holderGate() message revealing internal capability model (triaged LOW→MEDIUM)
  • 🟡 Same-pattern disabled-gate removal on create/update mutation buttons (AttributeEditor, ProfileEditor) and add-tile/bind controls in IdentityMap (triaged LOW→MEDIUM)

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