Skip to content

fix(console): the map shows where you are known, not everywhere you could be - #177

Merged
stormer78 merged 1 commit into
mainfrom
fix/persona-map-empty-contexts
Sep 7, 2026
Merged

fix(console): the map shows where you are known, not everywhere you could be#177
stormer78 merged 1 commit into
mainfrom
fix/persona-map-empty-contexts

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

From the first real screenshot of #176: twelve contexts, one persona, and eleven cards saying "Nobody yet. This context knows nothing about you." around the one that mattered.

The band's own label promises where you are known, and as whom. A context where nobody is known is not that, and on an agent with many contexts the empty cards were the whole page.

What changed

Empty contexts fold into a single row:

Not known in 11 other contexts. They hold nothing about you. — Be known somewhere else… · Show them

  • Be known somewhere else… asks which context first (offering the empty ones, since that is what the row is about) and then opens the same binding form a card's own button opens.
  • Show them reveals the cards as before, with a Hide in the band header to fold them back.
  • A holder known nowhere sees "You are not known anywhere yet. Nothing below the line holds a copy of anything." instead of a grid of identical empty cards.

A context the agent would not answer for stays visible in either state. "Could not ask" is not "nobody is known here", and folding it away would draw a picture that reads as complete when it is not — the one wrong answer the map must never give.

Not done here

A tree of contexts (the shell already knows parent/child) was the other option raised. It would organise the known ones better once there are many, but the problem in the screenshot was the empty ones, and a tree of twelve nodes with one lit is still twelve nodes. Worth revisiting when the known set is what's crowded.

Verification

npm run lint, npm run build, npm test — 836 across four workspaces; all six ci.yml assertions against the real build.

…ould be

On an agent with twelve contexts and one persona, the contexts band was
eleven cards saying "Nobody yet" around the one that mattered. The band's
own label promises "where you are known, and as whom", and a context
where nobody is known is not that.

The empty ones now fold into one row — "Not known in 11 other contexts.
They hold nothing about you." — with two actions: "Be known somewhere
else…", which asks which context first and then opens the same binding
form a card's own button opens, and "Show them", which reveals the cards
as before. A context the agent would not answer for stays visible in
either state: "could not ask" is not "nobody is known here", and folding
it away would draw a picture that reads as complete when it is not.

A holder known nowhere sees that said plainly instead of a grid of
identical empty cards.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit f787558 into main Sep 7, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/persona-map-empty-contexts branch September 7, 2026 11:46
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #177

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/persona-map-empty-contextsmain
Validated 2026-09-07
Scan ID ce8a77e5
Validator AI Security Validation Agent

🗺️ Scan Coverage

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

Module Files scanned Findings
packages/extension 1 2

Executive Summary

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

⚠️ 2 finding(s) need human review. These could not be conclusively confirmed or dismissed automatically (insufficient evidence). They are not dismissed — a developer / security team member must read and decide.


🔒 Security Issues

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

Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.

🟡 Authorization decision (denied) enforced only via UI disabled attribute, not re-verified at bind submission

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/panes/persona-map.tsx:612
Finding ID github_pr-13441d3c146d
CWE CWE-602, CWE-863
OWASP A01:2021 - Broken Access Control
MITRE ATT&CK T1548
CAPEC CAPEC-580, CAPEC-122
CVSS 4.0 5.3 (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)
DREAD 4.2
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE-863 (Incorrect Authorization) confirmed reachable at the UI layer via code evidence, but exploitability is scanner-rated 'low' (requires local DOM/devtools access, not remotely exploitable), there is no public exploit or CVE, and the ultimate impact depends on an unconfirmed downstream re-validation gap not shown in this file. Business impact is scanner-rated 'low' and scoped to a single user's own identity graph. This does not meet HIGH criteria (requires CVSS≥7 or confirmed significant impact plus exploit evidence) — it sits squarely at MEDIUM: real access-control weakness, specific conditions (local access + downstream flaw) required to fully exploit.
  • Composite score: 5.1
  • Environment: production

📝 Description:

If the actual bind-persistence call (not present in this excerpt) trusts UI-computed denied state, a user/agent whose holder authority should forbid new bindings could still complete a persona-to-context bind by bypassing the disabled button in devtools, resulting in an unauthorized identity disclosure relationship being recorded.

🧪 Proof of Concept:

The denied value is computed once via holderGate(authority) and used exclusively as a rendering/UX gate (disabled + title). The onClick handler itself contains no guard, meaning any code path that can invoke the handler bypassing React's disabled-button semantics (e.g., direct event dispatch, non-standard input devices, or automated DOM manipulation) will execute the state transition unconditionally.

<div style={{ display: "flex", gap: 6, marginLeft: "auto" }}>
  <Button
    kind="default"
    disabled={Boolean(denied) || graph.faces.length === 0}
    {...(denied ? { title: denied } : graph.faces.length === 0 ? { title: "Make a face first." } : {})}
    onClick={() => setEditing({ kind: "bind", contextId: null })}
  >
    Be known somewhere else…
  </Button>
  <Button kind="quiet" onClick={() => setShowEmpty(true)}>
    Show them
  </Button>
</div>

Vulnerable lines: 608, 622

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

<Button
  kind="default"
  disabled={Boolean(denied) || graph.faces.length === 0}
  {...(denied ? { title: denied } : graph.faces.length === 0 ? { title: "Make a face first." } : {})}
  onClick={() => setEditing({ kind: "bind", contextId: null })}
>
  Be known somewhere else…
</Button>

💥 Impact:

If the actual bind-persistence call (not present in this excerpt) trusts UI-computed denied state, a user/agent whose holder authority should forbid new bindings could still complete a persona-to-context bind by bypassing the disabled button in devtools, resulting in an unauthorized identity disclosure relationship being recorded.

Confidentiality: Low — could allow initiating a binding to an additional context that the user's persona should not be exposable to under current authority tier. · Integrity: Low — an unauthorized bind could create an incorrect persona/context association in the identity graph. · Availability: None

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-001 (Be known somewhere else… button) → setEditing({kind:'bind', contextId:null}) → EP-002 (ChooseContext) → EP-004 (BindingForm submit) — authority check only enforced as UI disabled attribute at EP-001

⚖️ Triage Factors:

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

Attack scenario: A local attacker with DOM/devtools access can bypass the disabled-only UI gate on the 'Be known somewhere else…' button to initiate a persona-to-context binding despite holderGate(authority) denying it, succeeding fully only if the downstream bind-persistence logic (not shown in this file) fails to re-validate authority.

🔧 Remediation:

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

The fix adds a redundant handler-level check (defense-in-depth) and, more importantly, requires that the actual authority check be re-performed at the point of persistence (background script / backend), not solely at the point of UI rendering. UI disabled attributes can always be bypassed by a client with DOM access; only re-validation at the trust boundary (background service worker or server) is a real control.

Vulnerable code:

<Button
  disabled={Boolean(denied) || graph.faces.length === 0}
  onClick={() => setEditing({ kind: "bind", contextId: null })}
>
  Be known somewhere else…
</Button>

Secure code:

// UI layer: keep disabled for UX, but ALSO gate the handler itself
<Button
  disabled={Boolean(denied) || graph.faces.length === 0}
  onClick={() => {
    if (denied || graph.faces.length === 0) return; // defense-in-depth, redundant with disabled
    setEditing({ kind: "bind", contextId: null });
  }}
>
  Be known somewhere else…
</Button>

// AND, critically, in the bind submission handler (e.g. BindingForm.onSubmit / background message handler):
async function submitBinding(personaDid: string, contextId: string, authority: Authority) {
  const denied = holderGate(authority);
  if (denied) {
    throw new Error(`Binding denied: ${denied}`);
  }
  // ... proceed with persistence only after server/background-side re-check
}

Additional recommendations:

  • Move holderGate(authority) evaluation into the extension's background/service-worker message handler that actually performs the bind, and reject if denied.
  • Consider signed/capability-scoped tokens for authority so the background context can verify authority without trusting renderer-supplied state.
  • Add integration tests that attempt to submit a bind action with denied true and assert rejection regardless of UI state.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: In packages/extension/src/manager/panes/persona-map.tsx the 'Be known somewhere else…' Button at line ~612 has disabled={Boolean(denied) || graph.faces.length === 0} where const denied = holderGate(authority); (line ~311). onClick sets setEditing({ kind: 'bind', contextId: null }), which flows into ChooseContext and then BindingForm (imported from './persona-editors.js'). EVIDENCE NOT FOUND: The implementation of holderGate (imported from '../holder-gate.js') and the implementation of BindingForm/its submission handler in './persona-editors.js' are not included in source_files, so it cannot be confirmed whether the actual bind-commit path (e.g. a background/service-worker message handler invoked by BindingForm) independently re-validates authority, or whether it is a pure client-only gate. CHANGED VS PRE-EXISTING: persona-map.tsx is the file under review for this PR (fix/persona-map-empty-contexts); the Button/denied logic appears in the file but holder-gate.js and persona-editors.js (containing BindingForm) are not in the diff's visible files, so the ultimate enforcement point (if any) can't be located. VERDICT JUSTIFICATION: Since the deciding sink (BindingForm's submission / underlying bind API) is not in provided files, this cannot be validated nor dismissed — classic 'must_review'.
  • 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.

🟡 No re-validation of selected contextId against current IdentityGraph state before BindingForm submission (potential TOCTOU)

Field Detail
Severity MEDIUM
Location packages/extension/src/manager/panes/persona-map.tsx:240
Finding ID github_pr-7a1da060c7ae
CWE CWE-367, CWE-20
OWASP A04:2021 - Insecure Design
MITRE ATT&CK T1499
CAPEC CAPEC-25
CVSS 4.0 5.1 (CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N)
DREAD 2.8
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE-367 TOCTOU here requires a race between user action and an async graph refresh — not directly triggerable by a crafted network request. Scanner rates exploitability as low and business impact as low (single-session data integrity only). No CVSS, no exploit maturity beyond theoretical, no public exploit. This aligns with medium: real design flaw with a reachable code path, but bounded impact and non-trivial exploit conditions keep it below high/critical thresholds.
  • Composite score: 5.2
  • Environment: production

📝 Description:

If graph.contexts is refreshed asynchronously between when ChooseContext is rendered and when the user clicks 'Next', the chosen contextId may reference a context that has since been removed, changed authorization scope, or is no longer valid, resulting in the persona being bound to an unintended or no-longer-existent context reference within the identity graph.

🧪 Proof of Concept:

chosen is derived purely from the contexts prop snapshot at mount/render time; there is no effect or handler that re-validates chosen against the live/current context list before onChoose propagates it upward into the bind flow.

function ChooseContext({ contexts, onChoose, onCancel }: {...}) {
  const [chosen, setChosen] = useState(contexts[0]?.id ?? "");
  return (
    <div style={{...}}>
      <span>Where?</span>
      <select value={chosen} onChange={(e) => setChosen(e.target.value)}>
        {contexts.map((ctx) => (<option key={ctx.id} value={ctx.id}>{ctx.label}</option>))}
      </select>
      <div style={{ display: "flex", gap: 8 }}>
        <Button kind="primary" disabled={!chosen} onClick={() => onChoose(chosen)}>Next</Button>
        <Button kind="quiet" onClick={onCancel}>Cancel</Button>
      </div>
    </div>
  );
}

Vulnerable lines: 225, 254

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

const [chosen, setChosen] = useState(contexts[0]?.id ?? "");
...
<Button kind="primary" disabled={!chosen} onClick={() => onChoose(chosen)}>Next</Button>

💥 Impact:

If graph.contexts is refreshed asynchronously between when ChooseContext is rendered and when the user clicks 'Next', the chosen contextId may reference a context that has since been removed, changed authorization scope, or is no longer valid, resulting in the persona being bound to an unintended or no-longer-existent context reference within the identity graph.

Confidentiality: Low — persona could be bound to an incorrect/stale context, altering who is presented as 'knowing' the persona. · Integrity: Low — a stale-context bind is a logic/data-integrity issue rather than a data corruption issue. · Availability: None

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-002 (ChooseContext select/Next) → onChoose(contextId) → setEditing({kind:'bind', contextId}) → EP-004 (BindingForm mount/submit) with no freshness check on contextId

⚖️ Triage Factors:

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

Attack scenario: A stale contextId captured by ChooseContext at render time can flow unchecked into BindingForm if the underlying IdentityGraph refreshes between selection and submission, potentially binding a persona to an unintended or no-longer-valid context.

🔧 Remediation:

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

Adding an existence/authorization re-check at both the transition point (onChoose) and at the actual submission point in BindingForm closes the TOCTOU window between context selection and bind persistence.

Vulnerable code:

onChoose={(contextId) => setEditing({ kind: "bind", contextId })}

Secure code:

onChoose={(contextId) => {
  const stillValid = graph.contexts.some((ctx) => ctx.id === contextId);
  if (!stillValid) {
    // surface an error / re-open ChooseContext with refreshed list
    setEditing(null);
    // e.g., toast: "That context is no longer available, please choose again."
    return;
  }
  setEditing({ kind: "bind", contextId });
}}

// Additionally, BindingForm.onSubmit should re-check existence/authorization
// against the freshest graph snapshot immediately before persisting.

Additional recommendations:

  • Version or timestamp the IdentityGraph snapshot and reject binds submitted against a stale version.
  • Surface a clear UI error state if the selected context disappears mid-flow, prompting re-selection.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: ChooseContext initializes const [chosen, setChosen] = useState(contexts[0]?.id ?? ""); and calls onChoose(chosen) on Next click; IdentityMap wires onChoose={(contextId) => setEditing({ kind: 'bind', contextId })} which is then consumed by BindingForm keyed on ${editing.contextId}:${editing.personaDid ?? 'new'}. No re-validation of contextId existence against a live graph snapshot is visible in persona-map.tsx before BindingForm mounts. EVIDENCE NOT FOUND: BindingForm's internal submission logic (in persona-editors.js, not provided) is not available, so it cannot be confirmed whether BindingForm re-validates contextId against fresh data before committing, nor whether the backend/API layer performs such validation. CHANGED VS PRE-EXISTING: persona-map.tsx (ChooseContext, IdentityMap render logic) is the file under review; BindingForm's implementation is out of scope of provided files. VERDICT JUSTIFICATION: Reachable UI flow is confirmed but the actual sink/validation logic is unverifiable from given sources — must_review, not validated nor dismissed.
  • 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.

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

🛡️ Threat Model & Affect Analysis — PR #177

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/persona-map-empty-contextsmain
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

Changes the persona/context map UI so that contexts where the user is not known are folded into a collapsed summary row instead of cluttering the grid, and introduces a two-step 'bind to a context' flow (generic 'Be known somewhere else…' entry point → ChooseContext selector → BindingForm) so a user can initiate binding without pre-selecting a context from a specific card.

Diff: +85 / -12 lines
Types: feature, ux, refactor

Risk Assessment

  • Overall Risk: medium
  • Review Priority: before_merge
  • Pentest Needed: true
  • Security Review Needed: true

The diff itself is a UI/UX-scoped change (context grouping + a two-step context-selection flow) with no new network calls, secrets, or infrastructure changes, which bounds its inherent risk. However, it adds a new entry point into a privacy-critical mutation flow (persona-to-context binding) whose only visible authorization control (holderGate) is applied purely at the client/UI layer. Because the actual commit logic (BindingForm and its backend/background handler) is not present in this diff, the change cannot be confirmed as safe in isolation — if the downstream commit trusts UI state, this change effectively adds a second, equally client-gated door into an already-flagged-as-high-risk mutation surface (per prior threat model STRIDE-8, high severity). This elevates the review priority to before-merge, though the diff's own code quality (defensive null-handling, disabled/title UX, explicit design comments) is otherwise solid.

Review Focus Areas:

  • Full implementation of BindingForm and its submission/commit path — this diff does not include it, and it is the true privileged mutation point downstream of this UI change.
  • Implementation of holderGate and confirmation of whether authority is independently re-checked at binding-commit time (background/service worker) rather than only in the renderer.
  • Source and trust boundary of IdentityGraph.contexts (labels, unreadable flag) — whether it is validated/sanitized before reaching this UI layer.

Pentest Focus:

  • Attempt to bypass the disabled state of the 'Be known somewhere else…' button via DOM manipulation or direct invocation of React state setters, and verify whether a subsequent BindingForm submission is rejected server-/background-side when authority is denied.
  • Attempt to submit a stale or manipulated contextId through the bind flow (e.g., by racing an IdentityGraph refresh against ChooseContext selection) and verify the binding is rejected if the context is no longer valid or authorized.
  • Test rendering of crafted ctx.label values (bidi overrides, homoglyphs, excessive length) in the ChooseContext dropdown to assess UI spoofing/deception risk.
  • Test behavior with a very large graph.contexts array when showEmpty is toggled true to assess UI responsiveness/DoS risk.

⚠️ Security Implications

🟠 New bind-initiation entry point gated only by client-side disabled attribute

New bind-initiation entry point gated only by client-side disabled attribute

Action: Enforce holderGate-equivalent authorization in the background/service-worker or backend handler that actually persists the binding, independent of any renderer-side state or disabled attributes.

🟡 contextId flowing from ChooseContext into BindingForm lacks re-validation

contextId flowing from ChooseContext into BindingForm lacks re-validation

Action: Re-validate the chosen contextId against the freshest IdentityGraph snapshot immediately before BindingForm submits, rejecting stale selections with a clear error.

🔵 Unsanitized ctx.label rendered in new selection dropdown

Unsanitized ctx.label rendered in new selection dropdown

Action: Sanitize/normalize labels (strip bidi/zero-width characters, cap display length, optionally show a disambiguating identifier) before rendering in selection UI.

🔵 isKnown heuristic distinguishes 'unreadable' from 'empty' contexts in UI grouping

isKnown heuristic distinguishes 'unreadable' from 'empty' contexts in UI grouping

Action: This is largely an accepted UX/privacy tradeoff; document it, and consider offering an aggregate-only disclosure mode for unreadable contexts if stricter privacy requirements apply.

⚪ Empty-context guard prevents BindingForm from mounting with a null contextId

Empty-context guard prevents BindingForm from mounting with a null contextId

Action: No action needed; maintain this pattern in future edits to the Editing state machine.

🧩 Affected Components

Component Impact Change What Changed
IdentityMap Contexts Band medium modified The 'Contexts' band now groups contexts into known (personas>0 or unreadable) vs empty, folding empty ones by default with a summary row and
Generic Bind-Elsewhere Flow high new New entry point ('Be known somewhere else…') that opens a context chooser (ChooseContext) before delegating to the existing BindingForm, ver

📁 File Classifications

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

  • Type: security

💡 Recommendations

  • MUST — Verify and, if absent, implement server-/background-side re-validation of holderGate-equivalent authority at the actual binding-commit point (inside or downstream of BindingForm), independent of the renderer's disabled-button state. (effort: medium)
    • Prevents unauthorized persona-to-context binding if the UI-only gate is bypassed via DOM manipulation, compromised co-installed extensions, or direct state/message injection.

✅ Positive Observations

  • The refactor correctly prevents BindingForm from ever mounting with contextId === null via explicit branching, avoiding a class of null-related runtime bugs.
  • In-code comments clearly document the privacy-conscious design rationale for distinguishing 'nobody known' from 'could not ask' (unreadable) contexts.
  • The existing disabled-state UX surfaces the denial reason via a title tooltip rather than failing silently, aiding user understanding.
  • No hardcoded secrets, credentials, or new network/deserialization code paths are introduced by this diff.
  • The change is narrowly scoped to presentation/UX logic plus a two-step selection flow, keeping the diff reviewable and limiting blast radius to this one file.

🛡️ STRIDE Threat Model

Identified Threats (10)

🟡 STRIDE-1: Client-Side Authorization Bypass in 'Be known somewhere else…' Button

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Likely
CVSS 5.3 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 Medium
CWE CWE-602,CWE-863
CAPEC CAPEC-580,CAPEC-122
OWASP A01:2021 - Broken Access Control

Description: 'Be known somewhere else…' button in IdentityMap allows binding-flow initiation via disabled-attribute-only enforcement due to reliance on the HTML disabled attribute and holderGate(authority) result computed client-side, resulting in unauthorized persona-to-context binding initiation if the underlying binding API does not independently re-verify authority.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~620-634

<Button kind="default" disabled={Boolean(denied) || graph.faces.length === 0} onClick={() => setEditing({ kind: "bind", contextId: null })}>Be known somewhere else…</Button>

Attack Scenario:

  1. Attacker inspects packages/extension/src/manager/panes/persona-map.tsx and observes const denied = holderGate(authority); used only to set the disabled prop and title attribute on the 'Be known somewhere else…' <Button>.
  2. Attacker opens browser/extension devtools, locates the rendered button element, and removes the disabled attribute via DOM manipulation or dispatches a synthetic click event directly on the React onClick handler bypassing the disabled check.
  3. onClick={() => setEditing({ kind: 'bind', contextId: null })} fires regardless of the denied value once the DOM-level disabled gate is bypassed, transitioning editing.kind to 'bind'.
  4. ChooseContext renders, attacker selects a contextId from emptyContexts/graph.contexts and clicks 'Next', invoking onChoose(contextId) which sets editing = { kind: 'bind', contextId }.
  5. BindingForm mounts with the attacker-chosen contextId and proceeds to persona binding without any confirmed server-side or extension-background re-validation of holderGate(authority) visible in this diff.
  6. If the downstream bind-submission API trusts the UI-computed authority state, attacker completes an unauthorized persona-to-context binding despite denied being truthy.

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

  • Data Flows: editing state transition: fact/face -> bind(contextId=null) -> bind(contextId set)

Preconditions: Attacker has access to the browser extension's rendered DOM/devtools (e.g., malicious local extension, compromised profile, or physical/session access)., The holderGate authority check is not independently re-enforced in the binding submission logic invoked from BindingForm.

Existing Controls: Client-side disabled attribute and title tooltip discourage casual misuse. • holderGate(authority) computed before rendering the affected button.

Recommended Mitigations: Re-validate holderGate(authority)-equivalent authorization at the point where BindingForm submits the bind action, not only in the UI trigger. • Perform authority checks in the extension background/service-worker context or backend before persisting any binding. • Add integrity checks (e.g., signed capability tokens) that the binding submission handler verifies independent of UI state.


🟡 STRIDE-2: Missing Validation of contextId Flowing from ChooseContext into BindingForm

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-367,CWE-20
CAPEC CAPEC-25
OWASP A04:2021 - Insecure Design

Description: ChooseContext onChoose callback in IdentityMap allows submission of an arbitrary or stale contextId due to lack of visible validation that the selected contextId still exists in graph.contexts or belongs to an authorized scope, resulting in binding to an unintended, stale, or attacker-influenced context.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~250-270, 688-694

const [chosen, setChosen] = useState(contexts[0]?.id ?? ""); ... onChoose={(contextId) => setEditing({ kind: "bind", contextId })}

Attack Scenario:

  1. ChooseContext is rendered with contexts={emptyContexts.length > 0 ? emptyContexts : graph.contexts} — attacker/automation manipulates the underlying graph state (e.g., via a race condition or a compromised upstream identity-graph provider) between render and selection.
  2. User selects a context via the <select> element; chosen state is set from e.target.value, which is the raw ctx.id string with no re-validation against the current graph snapshot at selection-commit time.
  3. Attacker triggers onChoose(chosen) (via Next button), invoking setEditing({ kind: 'bind', contextId: chosen }) without any code path shown that re-checks the contextId still exists, is not stale, or is authorized for the current persona/authority.
  4. BindingForm mounts keyed by ${editing.contextId}:${editing.personaDid ?? 'new'} and proceeds to bind against this unverified contextId.
  5. If graph.contexts was refreshed/mutated concurrently (e.g., an async IdentityGraph fetch resolved after ChooseContext rendered), the persona could be bound to a stale or attacker-controlled context reference, an instance of TOCTOU between selection and bind commit.

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

  • Data Flows: ChooseContext.onChoose(contextId) -> IdentityMap.editing -> BindingForm

Preconditions: Underlying graph/IdentityGraph data can change between the ChooseContext render and the BindingForm submission (e.g., async refresh, multiple tabs, background sync)., No re-validation exists downstream in BindingForm or bind API for context existence/authorization at submit time.

Existing Controls: contextId is scoped to values present in contexts prop at selection time via the <select> options.

Recommended Mitigations: Re-validate the selected contextId against the freshest IdentityGraph state at BindingForm submission, not just at ChooseContext render. • Use immutable snapshot/versioning of graph.contexts for the duration of the bind flow, rejecting stale selections. • Reject and surface an error if the chosen contextId no longer exists when BindingForm submits.


🔵 STRIDE-3: Information Disclosure via Unreadable-Context Visibility Heuristic in isKnown

Field Detail
Category Information Disclosure
Severity Low
Likelihood Possible
CVSS 3.1 CVSS:4.0/AV:L/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: Contexts band in IdentityMap allows inference of unreadable/private context existence due to the isKnown predicate exposing ctx.unreadable !== undefined as a distinguishing UI signal, resulting in disclosure of the existence and count of contexts the agent could not query, which may leak operational/network topology information to a party observing the UI (e.g., shoulder-surfing, screen-sharing, or a compromised companion extension reading the DOM).

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~316

const isKnown = (ctx) => ctx.personas.length > 0 || ctx.unreadable !== undefined;

Attack Scenario:

  1. isKnown is defined as ctx.personas.length > 0 || ctx.unreadable !== undefined, meaning contexts marked unreadable are kept in knownContexts and rendered as visible cards distinct from the folded emptyContexts bucket.
  2. An observer with access to the rendered UI (screen share, malicious co-installed extension with DOM read access, or shoulder surfing) can distinguish 'nobody knows you here' contexts from 'could not ask' contexts purely from which band a context card appears in.
  3. This reveals which third-party contexts/services actively refused or failed a lookup versus contexts that simply have no data, disclosing operational details about the user's relationship to external context providers (e.g., that a specific site is unreachable/blocking queries) that the user may not intend to reveal.
  4. Over multiple sessions, an attacker builds a fingerprint of which contexts are consistently 'unreadable', inferring network/blocking configuration or relationship metadata about the user's extension-connected services.

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

  • Data Flows: graph.contexts -> isKnown filter -> knownContexts/emptyContexts UI bands

Preconditions: Attacker has visual or DOM read access to the rendered IdentityMap pane (compromised extension, malicious browser extension with tabs/DOM permissions, or physical observation).

Existing Controls: No dedicated masking; behavior is an intentional UX design choice per code comments.

Recommended Mitigations: Treat 'unreadable' context disclosure as a deliberate UX tradeoff but document it in privacy documentation. • Consider aggregating unreadable-context counts without exposing per-context identity/labels unless the user explicitly expands the view. • Apply the same visual treatment/access control to unreadable contexts as to sensitive persona data if regulatory/privacy requirements demand it.


🔵 STRIDE-4: Denial of Service via Unbounded emptyContexts Rendering in IdentityMap

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

Description: Contexts band in IdentityMap allows client-side resource exhaustion via the 'Show them' toggle due to shownContexts = showEmpty ? graph.contexts : knownContexts rendering the full unbounded contexts array with no pagination/virtualization, resulting in degraded UI responsiveness or tab crash if graph.contexts is very large (e.g., a maliciously large IdentityGraph payload).

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~319, ~552

const shownContexts = showEmpty ? graph.contexts : knownContexts; ... {shownContexts.map((ctx) => { ... })}

Attack Scenario:

  1. An upstream IdentityGraph provider (or a compromised sync source) returns an artificially large contexts array (thousands of entries) with unreadable set or otherwise crafted to bypass folding.
  2. User (or an automated test/attacker script simulating clicks) clicks 'Show them', setting showEmpty = true.
  3. shownContexts becomes graph.contexts (the full unbounded list) and the grid shownContexts.map((ctx) => {...}) renders every context as a DOM card with no virtualization/pagination.
  4. Browser tab/extension popup becomes unresponsive or crashes due to excessive DOM node creation, degrading availability of the persona-map pane and potentially the whole extension popup process.

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

  • Data Flows: graph.contexts -> shownContexts -> DOM grid render

Preconditions: Attacker or compromised upstream source can inject or inflate the graph.contexts array to an excessive size., User or automated flow triggers setShowEmpty(true).

Existing Controls: Empty contexts are folded by default (showEmpty initial state false), reducing default rendering cost.

Recommended Mitigations: Add virtualization (windowing) or pagination for shownContexts rendering when the list exceeds a threshold. • Cap the number of contexts rendered per page with 'load more' functionality. • Validate/limit the size of graph.contexts at the IdentityGraph ingestion boundary.


🔵 STRIDE-5: Repudiation of Binding Initiation due to Absent Audit Logging in Bind Flow

Field Detail
Category Repudiation
Severity Low
Likelihood Possible
CVSS 2.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/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: BindingForm/ChooseContext binding-initiation flow in IdentityMap allows repudiation of a persona-to-context binding action due to absence of any visible audit/logging call in the reviewed code path (setEditing/onChoose transitions), resulting in inability to later prove which user/session initiated a specific binding decision.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~630, ~690

onClick={() => setEditing({ kind: "bind", contextId: null })} ... onChoose={(contextId) => setEditing({ kind: "bind", contextId })}

Attack Scenario:

  1. User (or attacker with transient access) clicks 'Be known somewhere else…', triggering setEditing({ kind: 'bind', contextId: null }) with no logging call visible in this diff.
  2. User selects a context in ChooseContext and clicks 'Next', invoking onChoose(contextId) — again no audit trail (timestamp, actor, chosen contextId) is recorded in the reviewed code.
  3. BindingForm proceeds to bind the persona to the context; if the binding later causes unwanted correlation/disclosure, there is no client-side evidence trail to establish who initiated the choice or when, complicating incident response and enabling the actor to deny initiating the binding.

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

  • Data Flows: User click -> setEditing -> onChoose -> BindingForm submission

Preconditions: No server-side or background-script logging exists for bind-flow initiation (assumption based on absence of evidence in this file; full audit posture cannot be confirmed from this excerpt).

Existing Controls: None visible in the reviewed diff.

Recommended Mitigations: Emit an auditable event (with actor/session id, timestamp, and chosen contextId) when onChoose fires and when BindingForm submits. • Persist binding-initiation events to a tamper-resistant log accessible for later review. • Correlate UI-level binding actions with backend audit records via a request id.


🔵 STRIDE-6: Spoofing of Context Identity via Unsanitized ctx.label Rendering in ChooseContext Dropdown

Field Detail
Category Spoofing
Severity Low
Likelihood Possible
CVSS 2.9 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-290,CWE-451
CAPEC CAPEC-148,CAPEC-98
OWASP A08:2021 - Software and Data Integrity Failures

Description: ChooseContext select options in IdentityMap allow context label spoofing via <option key={ctx.id} value={ctx.id}>{ctx.label}</option> due to rendering ctx.label from the IdentityGraph data without shown sanitization/validation of its origin, resulting in a user being deceived into selecting/binding to a different context than intended if ctx.label is attacker-influenced (e.g., a malicious or compromised context provider naming itself deceptively, such as spoofing a trusted provider's display name).

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~236-244

{contexts.map((ctx) => (<option key={ctx.id} value={ctx.id}>{ctx.label}</option>))}

Attack Scenario:

  1. A malicious or compromised external context/site registers itself in the user's IdentityGraph with a label crafted to impersonate a trusted, well-known context (e.g., naming itself identically to a bank or well-known service the user already trusts).
  2. User opens 'Be known somewhere else…' and is presented the ChooseContext dropdown listing contexts.map((ctx) => <option ...>{ctx.label}</option>), where the spoofed label is indistinguishable from the legitimate context's label.
  3. User, trusting the displayed label, selects the spoofed entry believing it is the legitimate service and clicks 'Next'.
  4. onChoose(contextId) binds the persona to the attacker-controlled context id rather than the intended legitimate one, causing the user's persona/attributes to be disclosed to or associated with the malicious context.

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

  • Data Flows: IdentityGraph.contexts[].label -> ChooseContext rendering -> user selection -> bind

Preconditions: An external/malicious context provider can register or influence its label field within the IdentityGraph consumed by this component., No uniqueness/verification of labels or origin badges exists in the dropdown UI (not shown in this diff).

Existing Controls: value={ctx.id} is used for the actual selection/bind action, so the underlying id is technically distinct even if labels collide (limits, but does not eliminate, impact).

Recommended Mitigations: Display a disambiguating origin indicator (e.g., domain/verified badge or truncated id) alongside ctx.label in the dropdown. • Deduplicate/flag contexts with colliding or highly similar labels before rendering. • Validate/sanitize ctx.label at IdentityGraph ingestion to strip deceptive formatting (e.g., RTL overrides, homoglyphs).


⚪ STRIDE-7: Tampering via React key Collision in BindingForm Remounting Logic

Field Detail
Category Tampering
Severity Informational
Likelihood Unlikely
CVSS 1.6 CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-664
CAPEC CAPEC-74
OWASP A04:2021 - Insecure Design

Description: BindingForm remount key in IdentityMap allows stale component state persistence via the composite key ${editing.contextId}:${editing.personaDid ?? "new"} due to potential key collisions when switching between bind targets with identical contextId/personaDid combinations in rapid succession, resulting in React reusing internal component state across logically distinct binding operations.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~693

key={`${editing.contextId}:${editing.personaDid ?? "new"}`}

Attack Scenario:

  1. BindingForm is keyed with key={${editing.contextId}:${editing.personaDid ?? "new"}}, intended to force remount when the bind target changes.
  2. If the same contextId/personaDid pair is revisited across successive edits without an intervening navigation that changes the key (e.g., cancel-and-reselect the same context and persona quickly), React may not fully reset internal form state (e.g., uncommitted field values) between logically separate user intents.
  3. Residual state from a prior aborted binding attempt could be inadvertently reused or displayed in a subsequent binding session for the same key, potentially leading to submission of stale or unintended data if BindingForm does not fully reset internal state on prop changes alone.

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

  • Data Flows: editing.contextId/personaDid -> BindingForm key -> component state

Preconditions: BindingForm's internal state management does not fully reset on identical key re-mount scenarios., User or automated flow rapidly cancels and re-initiates binding for the same context/persona pair.

Existing Controls: Composite key already reduces most collision scenarios by scoping to contextId and personaDid.

Recommended Mitigations: Include a monotonic session/edit-attempt counter in the BindingForm key to guarantee remount on every new editing session. • Explicitly reset BindingForm internal state in a useEffect keyed on the same identifiers as a defense-in-depth measure.


🟠 STRIDE-8: Elevation of Privilege via Client-Only Enforcement of holderGate Across Entire Bind Flow

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

Description: Persona-to-context binding flow in IdentityMap allows privilege escalation via a fully client-side gate due to holderGate(authority) being the only visible authorization check gating the 'Be known somewhere else…' entry point with no server-side re-validation demonstrated in the provided code, resulting in a low-privilege or restricted 'holder' actor completing an unauthorized binding operation that should have been denied.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~311

const denied = holderGate(authority);

Attack Scenario:

  1. const denied = holderGate(authority); computes a client-side authorization decision from the authority prop passed into IdentityMap.
  2. This value is used only to set disabled/title on the UI trigger button — no equivalent check is shown being re-applied inside ChooseContext.onChoose or at BindingForm submission time within this diff.
  3. An attacker who can invoke the underlying state setters directly (e.g., via a compromised extension content script with access to the React fiber/dev tools, or by directly calling the exported binding API/message handler if one exists in the background service worker) bypasses the UI-only gate entirely.
  4. The bind action proceeds and is persisted (assuming the extension's storage/background messaging layer trusts the UI-initiated payload), granting the restricted actor an unauthorized persona-context binding — effectively an elevation of privilege relative to the intended authority-based restriction.
  5. Full confirmation requires the holderGate implementation and the message-passing/background handler that ultimately commits the binding, neither of which is present in this file per the recon notes; this threat is flagged as high-impact-if-true and should be validated against the full binding pipeline.

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

  • Data Flows: authority prop -> holderGate -> disabled UI gate (only) -> bind commit

Preconditions: The binding commit pathway (background script, IndexedDB write, or extension messaging API) trusts the renderer-provided bind request without independently re-checking holderGate-equivalent authority., Attacker has some code-execution or message-injection capability within the extension's trust boundary (e.g., malicious co-installed extension, compromised content script, or exposed messaging endpoint).

Existing Controls: holderGate(authority) is computed and referenced before allowing the UI entry point to be actionable.

Recommended Mitigations: Enforce holderGate-equivalent authority checks in the background/service-worker handler that actually commits the persona-context binding, independent of any renderer-side state. • Treat all runtime.sendMessage/messaging-layer bind requests as untrusted input requiring full re-authorization server-side (within the background context). • Add integration tests asserting that a denied authority cannot produce a persisted binding even when UI checks are bypassed.


⚪ STRIDE-9: Denial of Service via Toggling showEmpty During Concurrent graph Mutation (Render Thrash)

Field Detail
Category Denial of Service
Severity Informational
Likelihood Unlikely
CVSS 1.6 CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1050,CWE-400
CAPEC CAPEC-130
OWASP A04:2021 - Insecure Design

Description: showEmpty toggle in IdentityMap allows repeated forced re-computation of knownContexts/emptyContexts/shownContexts via .filter calls on every render due to these derivations not being memoized (no useMemo shown), resulting in unnecessary CPU churn that could be amplified by an attacker forcing rapid re-renders (e.g., via a scripted spam of the toggle or upstream graph updates), degrading UI responsiveness.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~316-319

const knownContexts = graph.contexts.filter(isKnown); const emptyContexts = graph.contexts.filter((ctx) => !isKnown(ctx)); const shownContexts = showEmpty ? graph.contexts : knownContexts;

Attack Scenario:

  1. knownContexts, emptyContexts, and shownContexts are recomputed via .filter on every render of IdentityMap since they are not wrapped in useMemo.
  2. An attacker with UI automation capability (e.g., malicious script driving synthetic events) rapidly toggles showEmpty or triggers frequent upstream graph prop updates.
  3. Each toggle/update forces re-filtering of the full graph.contexts array plus a full re-render of shownContexts.map(...), and combined with useBoxes layout recalculation (useBoxes(stage, [graph, editing, selection?.kind])), this can compound into noticeable UI jank or unresponsiveness under a large context set.

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

  • Data Flows: graph.contexts -> filter (isKnown) -> knownContexts/emptyContexts/shownContexts (per-render)

Preconditions: graph.contexts is large., Attacker or malicious automation can trigger rapid repeated toggles or graph prop churn.

Existing Controls: Default showEmpty=false limits the default rendering cost.

Recommended Mitigations: Wrap isKnown-derived filters in useMemo keyed on graph.contexts to avoid redundant recomputation. • Debounce or rate-limit showEmpty toggle handling if driven by rapid programmatic events. • Memoize shownContexts list rendering with React.memo per-card components.


🔵 STRIDE-10: Repudiation/Prompt-Injection-Style Manipulation via Untrusted Context Label Text Rendered as UI Copy

Field Detail
Category Tampering, Spoofing
Severity Low
Likelihood Possible
CVSS 3.5 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-116,CWE-451
CAPEC CAPEC-19,CAPEC-98
OWASP A03:2021 - Injection

Description: BandLabel/ChooseContext rendering of ctx.label in IdentityMap allows deceptive UI text injection due to interpolation of externally-sourced label strings directly into security-relevant confirmation/selection UI without shown length limits, RTL/bidi sanitization, or content policy enforcement, resulting in a manipulated or confusing consent flow where the user misunderstands which context they are binding to.

Evidence: packages/extension/src/manager/panes/persona-map.tsx:~236-244, ~538-560

<option key={ctx.id} value={ctx.id}>{ctx.label}</option>

Attack Scenario:

  1. A malicious or compromised context provider sets ctx.label to a crafted string (e.g., containing Unicode right-to-left override characters, excessive whitespace, or text mimicking a system message such as 'Cancel — do not bind').
  2. This label flows unmodified into ChooseContext's <option>{ctx.label}</option> and into the surrounding Contexts band UI (emptyContexts.length messaging area) wherever context labels are interpolated.
  3. Because React escapes HTML by default, script injection (XSS) is not directly possible via {ctx.label}, but visual spoofing/deception via crafted Unicode or misleading text remains possible, causing the user to misinterpret the binding target or believe they are canceling when they are actually confirming.
  4. User completes the bind flow believing they selected a different or no context, resulting in an unintended persona-context binding they did not consciously consent to.

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

  • Data Flows: IdentityGraph.contexts[].label -> ChooseContext/BandLabel UI text rendering

Preconditions: Attacker/malicious context provider can set arbitrary label values ingested into IdentityGraph., No sanitization of bidi control characters or length constraints is applied before rendering (not shown in this diff).

Existing Controls: React's default JSX text-node escaping prevents HTML/script injection (XSS) via {ctx.label}.

Recommended Mitigations: Sanitize/strip bidirectional control characters and zero-width characters from ctx.label before rendering. • Enforce a maximum display length and truncate with ellipsis plus full value on hover/tooltip. • Normalize and visually flag labels containing non-printable or suspicious Unicode ranges.



🍝 PASTA Threat Model

Application Purpose

A browser extension identity-management pane that lets users visualize and manage which 'contexts' (external sites/services) know them, under which personas, and bind personas to new contexts, providing privacy-preserving identity correlation control.

Inherent Risks

  • The extension mediates sensitive persona-to-context binding decisions where UI-only enforcement gaps can lead to unauthorized identity correlation.
  • IdentityGraph data (contexts, labels, unreadable status) originates from external/third-party sources and is rendered largely as-is in security-relevant UI.
  • No visibility into the background/service-worker enforcement layer means client-side gates may be the only control in practice.

Objectives

Risk: Limit exposure of unauthorized binding actions and misleading consent flows to Medium or lower residual severity.
Business: Provide users trustworthy visibility and control over where their identity/personas are known.
Security: Ensure persona-to-context binding actions are authorized consistently across UI and backend layers.
Financial: Avoid liability from unauthorized identity correlation or privacy breaches attributable to the extension.
Compliance: Support privacy-by-design principles consistent with data minimization expectations for identity/correlation data.
Functional: Allow users to bind a persona to a new or existing context via a guided two-step flow.
Operational: Maintain responsive UI performance even with large identity graphs.

Business Impact Analysis (2)

BIA-1: Persona-to-Context Binding Authorization (High)

The end-to-end process by which a user selects a context and binds one of their personas to it, governed by the holderGate authority check.

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

  • Stakeholders: Extension Development Team / Extension Users / Third-Party Context Providers
  • Dependencies: BindingForm Component / ChooseContext Component / IdentityGraph Data Source / holderGate Authorization Function
  • Disruptions: Client-side authority gate bypass allowing unauthorized binding / Stale or manipulated contextId reaching BindingForm / Background/service-worker binding commit trusting unverified UI state
  • Impacts: Unauthorized disclosure of persona attributes to unintended contexts / Loss of user trust in the extension's privacy guarantees / Potential regulatory exposure if bound data constitutes personal data under privacy law

BIA-2: Contexts Visibility and Disclosure Presentation (Medium)

The process of filtering and displaying which contexts know the user versus which are empty or unreadable, balancing UX clarity against information disclosure.

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

  • Stakeholders: Extension Users / Extension Development Team
  • Dependencies: IdentityGraph Data Source / IdentityMap Component isKnown Filter Logic
  • Disruptions: Overexposure of unreadable-context metadata to observers / Unbounded rendering of large context lists causing UI degradation
  • Impacts: Minor privacy leakage of operational relationship metadata / Degraded UI responsiveness under adversarial data volume

Technical Scope

Roles (2): RO-1 Holder (Authorized Persona Owner) · RO-2 Restricted/Denied Actor

Actors (3): AC-1 Extension User · AC-2 Malicious Local Actor · AC-3 IdentityGraph Sync Service

Entry Points (4): EP-001 Be Known Somewhere Else Button · EP-002 ChooseContext Selection · EP-003 Show/Hide Empty Contexts Toggle · EP-004 BindingForm Submission Mount

Threat Actors (3): TA-1 Malicious Co-Installed Extension · TA-2 Compromised Context Provider · TA-3 Local Device Attacker

Infrastructure (1): IF-1 Browser Extension Runtime

Trust Boundaries (2): TB-1 Browser Extension UI Boundary · TB-2 External IdentityGraph Data Boundary

External Entities (2): EE-1 Third-Party Context Provider · EE-2 Extension Background/Service Worker

System Components (5): SC-1 IdentityMap Pane · SC-2 ChooseContext Dialog · SC-3 BindingForm Component · SC-4 holderGate Authorization Function · SC-5 IdentityGraph Data Store

Resources And Assets (3): RA-1 IdentityGraph Contexts Array · RA-2 Persona-Context Binding Record · RA-3 Authority/holderGate Decision State

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

Use Cases (2)

  • Persona Binding via Guided Context Selection: An authorized user clicks 'Be known somewhere else…', selects a context via ChooseContext, and completes the binding through BindingForm.
  • Contexts Visibility Toggle: A user toggles the 'Show them'/'Hide' control to reveal or fold contexts where no persona is currently known.

📋 Risk Registry (6)

ID Title Severity Residual Priority Effort
RISK-001 Persona-to-context binding may be authorized solely by client-side UI state, enabling unauthorized binding if the backend trusts the renderer. High High Immediate Medium
RISK-002 Selected contextId may become stale or unauthorized between selection and binding submission (TOCTOU). Medium Low Short-Term Low
RISK-003 Contexts marked unreadable are visually distinguishable from empty contexts, potentially disclosing operational metadata to observers. Low Low Long-Term Low
RISK-004 Unsanitized externally-sourced context labels rendered in selection UI could deceive users into binding to spoofed contexts. Low Low Medium-Term Medium
RISK-005 Large or adversarially-inflated context lists rendered without virtualization could degrade or crash the extension UI. Low Low Long-Term Low
RISK-006 Absence of visible audit logging for bind-flow initiation actions impedes incident investigation and accountability. Low Low Medium-Term Medium

⚔️ Attack Scenarios (3)

SC-1: IdentityMap Pane

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: IdentityMap Pane" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE863@{ shape: rect, label: "CWE-863: Incorrect Authorization" }
    CWE200@{ shape: rect, label: "CWE-200: Information Exposure" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC122@{ shape: rect, label: "CAPEC-122: Privilege Abuse" }
    CAPEC118@{ shape: rect, label: "CAPEC-118: Data Leakage Attacks" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S1@{ shape: rect, label: "STRIDE-1: Client-Side Authorization Bypass in 'Be known somewhere else…' Button<br><i>Medium / Likely</i>" }
    S3@{ shape: rect, label: "STRIDE-3: Information Disclosure via Unreadable-Context Visibility Heuristic<br><i>Low / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA3@{ shape: rect, label: "TA-3: Local Device Attacker<br><i>Bypass disabled-button-only restrictions</i>" }
    TA1@{ shape: rect, label: "TA-1: Malicious Co-Installed Extension<br><i>Harvest binding data</i>" }
  end
  SC1 --> CWE863
  CWE863 --> CAPEC122
  CAPEC122 --> S1
  S1 --> TA3
  SC1 --> CWE200
  CWE200 --> CAPEC118
  CAPEC118 --> S3
  S3 --> 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:#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-3: BindingForm Component

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC3@{ shape: rect, label: "SC-3: BindingForm Component" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE862@{ shape: rect, label: "CWE-862: Missing Authorization" }
    CWE367@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC58@{ shape: rect, label: "CAPEC-58: Restful Privilege Elevation" }
    CAPEC25@{ shape: rect, label: "CAPEC-25: Forced Deadlock/Race Exploit" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S8@{ shape: rect, label: "STRIDE-8: Elevation of Privilege via Client-Only Enforcement of holderGate<br><i>High / Possible</i>" }
    S2@{ shape: rect, label: "STRIDE-2: Missing Validation of contextId Flowing into BindingForm<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Co-Installed Extension<br><i>Harvest or manipulate binding data</i>" }
    TA3@{ shape: rect, label: "TA-3: Local Device Attacker<br><i>Manipulate rendered UI/DOM</i>" }
  end
  SC3 --> CWE862
  CWE862 --> CAPEC58
  CAPEC58 --> S8
  S8 --> TA1
  SC3 --> CWE367
  CWE367 --> CAPEC25
  CAPEC25 --> S2
  S2 --> TA3
  linkStyle 0 stroke:#FF0000, stroke-width:2px
  linkStyle 1 stroke:#FF0000, stroke-width:2px
  linkStyle 2 stroke:#FF0000, stroke-width:2px
  linkStyle 3 stroke:#FF0000, stroke-width:2px
  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

SC-2: ChooseContext Dialog

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC2@{ shape: rect, label: "SC-2: ChooseContext Dialog" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE290@{ shape: rect, label: "CWE-290: Authentication Bypass by Spoofing" }
    CWE116@{ shape: rect, label: "CWE-116: Improper Output Encoding" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC148@{ shape: rect, label: "CAPEC-148: Content Spoofing" }
    CAPEC98@{ shape: rect, label: "CAPEC-98: Phishing" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S6@{ shape: rect, label: "STRIDE-6: Spoofing of Context Identity via Unsanitized ctx.label<br><i>Low / Possible</i>" }
    S10@{ shape: rect, label: "STRIDE-10: Deceptive UI Text Injection via Untrusted Context Label<br><i>Low / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: Compromised Context Provider<br><i>Deceive users via spoofed labels</i>" }
  end
  SC2 --> CWE290
  CWE290 --> CAPEC148
  CAPEC148 --> S6
  S6 --> TA2
  SC2 --> CWE116
  CWE116 --> CAPEC98
  CAPEC98 --> S10
  S10 --> TA2
  linkStyle 0 stroke:#00FF00, stroke-width:2px
  linkStyle 1 stroke:#00FF00, stroke-width:2px
  linkStyle 2 stroke:#00FF00, stroke-width:2px
  linkStyle 3 stroke:#00FF00, 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

📊 Risk Summary

Total Threats: 10

By Severity: Low: 5 · High: 1 · Medium: 2 · Informational: 2

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

🎯 Attack Surface

Kill Chain 1: A local or co-installed-extension attacker (TA-1/TA-3) begins by bypassing the purely client-side disabled gate on the 'Be known somewhere else…' button (EP-001), which is driven solely by holderGate(authority) (STRIDE-1, STRIDE-8); this single DOM/state manipulation step immediately unlocks the entire bind flow regardless of the user's actual authority, chaining directly into the ChooseContext dialog (EP-002) where the attacker selects an arbitrary contextId from emptyContexts/graph.contexts (STRIDE-2) with no visible re-validation, and finally into BindingForm (EP-004) where the binding is submitted — if the background/service-worker binding commit trusts this UI-originated state, the attacker achieves full unauthorized persona-context binding, the most severe realistic outcome in this diff. Kill Chain 2: A compromised or malicious third-party context provider (TA-2) supplies a crafted label field into the IdentityGraph (RA-1) that is rendered verbatim in the ChooseContext <select> options (STRIDE-6, STRIDE-10); a legitimate user, deceived by a spoofed or visually manipulated label, selects the malicious context believing it to be trusted, and completes the bind flow, resulting in unintended persona disclosure to the attacker-controlled context — this chain requires no code execution, only data-layer manipulation of IdentityGraph inputs consumed by SC-1/SC-2. Kill Chain 3: An observer with DOM/screen access (TA-1) leverages the isKnown heuristic (STRIDE-3) to distinguish 'unreadable' contexts from truly empty ones, and separately, an attacker able to inflate graph.contexts size can force showEmpty=true rendering to degrade UI performance (STRIDE-4, STRIDE-9); while individually low severity, these can be combined with Kill Chain 1 as a distraction/DoS layer to mask a concurrent unauthorized-binding attempt, and the lack of audit logging (STRIDE-5) means none of these chained actions would be reliably attributable after the fact.

🛡️ Risk Mitigation Strategy

Priority 1 (Immediate): The most critical control gap is that holderGate(authority) appears to be enforced only in the renderer to toggle a disabled attribute, with no visible re-validation at the point where BindingForm actually commits a persona-context binding; this must be remediated first by moving authoritative enforcement into the extension's background/service-worker layer (or equivalent trusted boundary) so that a bypassed or manipulated UI state can never result in a persisted unauthorized binding, closing RISK-001 which drives the highest-severity STRIDE-1/STRIDE-8 threats. Priority 2 (Short-Term): Address the TOCTOU gap between context selection in ChooseContext and binding commit in BindingForm (RISK-002/STRIDE-2) by re-validating the chosen contextId against the freshest IdentityGraph snapshot immediately before submission, and reject stale or non-existent selections with clear user feedback, preventing silent binding to unintended contexts. Priority 3 (Medium-Term): Harden the data-layer trust assumptions around externally-sourced ctx.label values (RISK-004/STRIDE-6/STRIDE-10) by sanitizing bidirectional/zero-width Unicode characters, enforcing display length limits, and optionally surfacing an origin/verification indicator, while also introducing structured audit logging for bind-flow initiation and completion (RISK-006/STRIDE-5) to support future incident investigation and non-repudiation. Priority 4 (Long-Term): Improve resilience and privacy hygiene with lower-urgency, defense-in-depth improvements: memoize context-filtering derivations and add virtualization/pagination for large context lists to mitigate rendering-based denial-of-service risk (RISK-005/STRIDE-4/STRIDE-9), and reassess whether the isKnown heuristic's exposure of unreadable context metadata (RISK-003/STRIDE-3) requires additional aggregation or user-controlled disclosure granularity to minimize incidental privacy leakage.


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

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

Must-Review-By-Human (2)

  • 🟡 Authorization decision (denied) enforced only via UI disabled attribute, not re-verified at bind submission
  • 🟡 No re-validation of selected contextId against current IdentityGraph state before BindingForm submission (potential TOCTOU)

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