fix(console): the map shows where you are known, not everywhere you could be - #177
Conversation
…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>
🛡️ AI Agentic Security Code Review2 findings need a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #177
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 1 · findings: 2
Executive Summary
🔒 Security Issues
|
| 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
deniedtrue 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}whereconst denied = holderGate(authority);(line ~311). onClick setssetEditing({ kind: 'bind', contextId: null }), which flows into ChooseContext and then BindingForm (imported from './persona-editors.js'). EVIDENCE NOT FOUND: The implementation ofholderGate(imported from '../holder-gate.js') and the implementation ofBindingForm/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:
ChooseContextinitializesconst [chosen, setChosen] = useState(contexts[0]?.id ?? "");and callsonChoose(chosen)on Next click;IdentityMapwiresonChoose={(contextId) => setEditing({ kind: 'bind', contextId })}which is then consumed byBindingFormkeyed 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.
Details
🛡️ Threat Model & Affect Analysis — PR #177
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch |
fix/persona-map-empty-contexts → main
|
| 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
BindingFormand 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
holderGateand 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.labelvalues (bidi overrides, homoglyphs, excessive length) in the ChooseContext dropdown to assess UI spoofing/deception risk. - Test behavior with a very large
graph.contextsarray whenshowEmptyis 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 === nullvia 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
titletooltip 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:
- Attacker inspects packages/extension/src/manager/panes/persona-map.tsx and observes
const denied = holderGate(authority);used only to set thedisabledprop andtitleattribute on the 'Be known somewhere else…'<Button>. - Attacker opens browser/extension devtools, locates the rendered button element, and removes the
disabledattribute via DOM manipulation or dispatches a synthetic click event directly on the ReactonClickhandler bypassing the disabled check. -
onClick={() => setEditing({ kind: 'bind', contextId: null })}fires regardless of thedeniedvalue once the DOM-level disabled gate is bypassed, transitioningediting.kindto'bind'. -
ChooseContextrenders, attacker selects acontextIdfromemptyContexts/graph.contextsand clicks 'Next', invokingonChoose(contextId)which setsediting = { kind: 'bind', contextId }. -
BindingFormmounts with the attacker-chosencontextIdand proceeds to persona binding without any confirmed server-side or extension-background re-validation ofholderGate(authority)visible in this diff. - If the downstream bind-submission API trusts the UI-computed authority state, attacker completes an unauthorized persona-to-context binding despite
deniedbeing 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:
-
ChooseContextis rendered withcontexts={emptyContexts.length > 0 ? emptyContexts : graph.contexts}— attacker/automation manipulates the underlyinggraphstate (e.g., via a race condition or a compromised upstream identity-graph provider) between render and selection. - User selects a context via the
<select>element;chosenstate is set frome.target.value, which is the rawctx.idstring with no re-validation against the current graph snapshot at selection-commit time. - Attacker triggers
onChoose(chosen)(viaNextbutton), invokingsetEditing({ 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. -
BindingFormmounts keyed by${editing.contextId}:${editing.personaDid ?? 'new'}and proceeds to bind against this unverified contextId. - If
graph.contextswas 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:
-
isKnownis defined asctx.personas.length > 0 || ctx.unreadable !== undefined, meaning contexts markedunreadableare kept inknownContextsand rendered as visible cards distinct from the foldedemptyContextsbucket. - 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.
- 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.
- 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:
- An upstream IdentityGraph provider (or a compromised sync source) returns an artificially large
contextsarray (thousands of entries) withunreadableset or otherwise crafted to bypass folding. - User (or an automated test/attacker script simulating clicks) clicks 'Show them', setting
showEmpty = true. -
shownContextsbecomesgraph.contexts(the full unbounded list) and the gridshownContexts.map((ctx) => {...})renders every context as a DOM card with no virtualization/pagination. - 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:
- 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. - User selects a context in
ChooseContextand clicks 'Next', invokingonChoose(contextId)— again no audit trail (timestamp, actor, chosen contextId) is recorded in the reviewed code. -
BindingFormproceeds 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:
- A malicious or compromised external context/site registers itself in the user's IdentityGraph with a
labelcrafted to impersonate a trusted, well-known context (e.g., naming itself identically to a bank or well-known service the user already trusts). - User opens 'Be known somewhere else…' and is presented the
ChooseContextdropdown listingcontexts.map((ctx) => <option ...>{ctx.label}</option>), where the spoofed label is indistinguishable from the legitimate context's label. - User, trusting the displayed label, selects the spoofed entry believing it is the legitimate service and clicks 'Next'.
-
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:
-
BindingFormis keyed withkey={${editing.contextId}:${editing.personaDid ?? "new"}}, intended to force remount when the bind target changes. - If the same
contextId/personaDidpair 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. - 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:
-
const denied = holderGate(authority);computes a client-side authorization decision from theauthorityprop passed intoIdentityMap. - This value is used only to set
disabled/titleon the UI trigger button — no equivalent check is shown being re-applied insideChooseContext.onChooseor atBindingFormsubmission time within this diff. - 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.
- 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. - Full confirmation requires the
holderGateimplementation 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:
-
knownContexts,emptyContexts, andshownContextsare recomputed via.filteron every render ofIdentityMapsince they are not wrapped inuseMemo. - An attacker with UI automation capability (e.g., malicious script driving synthetic events) rapidly toggles
showEmptyor triggers frequent upstreamgraphprop updates. - Each toggle/update forces re-filtering of the full
graph.contextsarray plus a full re-render ofshownContexts.map(...), and combined withuseBoxeslayout 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:
- A malicious or compromised context provider sets
ctx.labelto 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'). - This label flows unmodified into
ChooseContext's<option>{ctx.label}</option>and into the surrounding Contexts band UI (emptyContexts.lengthmessaging area) wherever context labels are interpolated. - 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. - 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
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
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
📊 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 | |
|---|---|
| 0 | 2 |
Must-Review-By-Human (2)
- 🟡 Authorization decision (
denied) enforced only via UIdisabledattribute, not re-verified at bind submission - 🟡 No re-validation of selected contextId against current IdentityGraph state before BindingForm submission (potential TOCTOU)
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:
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 sixci.ymlassertions against the real build.