Skip to content

fix(persona): read the disclosure context from its typed shape, and check the type - #190

Merged
stormer78 merged 2 commits into
mainfrom
fix/persona-step-up-context-shape
Sep 8, 2026
Merged

fix(persona): read the disclosure context from its typed shape, and check the type#190
stormer78 merged 2 commits into
mainfrom
fix/persona-step-up-context-shape

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Pairs with OpenVTC/verifiable-trust-infrastructure#1306, which fixes a defect in
the agent-side landing (VTI #1304): the disclosure's authorization context was a
flat bag with no type, and an approver's card discriminates on type.

The agent now sends the shape every authorization context uses:

{ "type": "https://openvtc.org/persona/authorization-context/0.1",
  "summary": "Approve disclosing 1 fact to did:key:z…",
  "risk": "high",
  "action": { "kind": "disclose", "previewId": "", "verifierDid": "",
              "claimTypes": ["payment.card"], "purpose": "checkout" } }

verifyDisclosureStepUp reads it from there, and gains the check the type
makes possible.

The new refusal is the point of the change

A context whose type is not the disclosure one is refused. Authorization
contexts are a shared channel — a Cierge share ask travels under the same
org.openvtc.authorization-context key. Without this check, one of those would
be read as a disclosure: shown to the holder in a disclosure's words, its
action mined for claim types it never had, and approved as though it were a
release of their identity.

The new test drives exactly that — a genuine, correctly-signed share ask, from
an enrolled agent — and requires it to be refused. Note that every other guard
in this module passes it: the proof verifies, the issuer is enrolled, nothing
is tampered. Only the type tells them apart.

summary is now surfaced on DisclosureApprovalContext too, since the agent
guarantees it equals the request's reason — a surface may show either without
the two differing.

Cut over, not folded

Per this repo's rule: nothing is deployed, so there is no dual-accept arm for
the old flat shape and no deprecation window. #1306 and this land together.

Tests

10 in persona.step-up.mjs (one new: the share-ask refusal); the previewId
cross-check and the tampered-context test move to action and still pass.
Full suite green — 915 across the four workspaces, 0 failures; tsc -b clean.

Guide checklist (§9)

  • R1.2/R1.3/R1.4/R1.5/R1.6 — no fetch, lock, retry, loop or ack touched
  • R2.1 — no mutation
  • R3.7 — discrimination is on a stable URI, never message text; the
    wire change cuts over with #1306 in the same cycle
  • R5.* — an unrecognised or absent type refuses rather than falling back
    to a permissive read; an absent action yields empty claimTypes
  • R6.* — only the verified, correctly-typed context is renderable
  • Deviations — none

…heck the type

Pairs with OpenVTC/verifiable-trust-infrastructure#1306, which gives the
disclosure's authorization context the `{type, summary, risk, action}` shape an
approver's card can render.

The check that shape makes possible is the point: authorization contexts share
one `ext` key, so a Cierge share ask arrives down the same path as a
disclosure. Without discriminating on `type`, a correctly-signed share ask from
an enrolled agent passes every other guard in this module — proof, issuer,
tamper — and would be shown to the holder in a disclosure's words, its `action`
mined for claim types it never had. Now it is refused, and a test drives it.

`summary` is surfaced on the context too: the agent guarantees it equals the
request's `reason`, so a surface may show either.

No compatibility fold for the old flat shape — nothing is deployed, and the two
repos cut over together.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 AI-confirmed issues.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #190

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/persona-step-up-context-shapemain
Validated 2026-09-08
Scan ID 43f956cd
Validator AI Security Validation Agent

🗺️ Scan Coverage

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

Module Files scanned Findings
packages/core 2 3

Executive Summary

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

🔒 Security Issues

Confirmed Vulnerabilities (2)

🟡 action.kind is never validated to equal 'disclose' after type gate — potential semantic type confusion

Field Detail
Severity MEDIUM
Location packages/core/src/persona/step-up.ts:185
Finding ID github_pr-bcd3292ab67b
CWE CWE-697, CWE-840
OWASP A08:2021 – Software and Data Integrity Failures
MITRE ATT&CK T1565: Data Manipulation
CAPEC CAPEC-122: Privilege Abuse, CAPEC-141: Cache Poisoning (analogous state-confusion pattern)
DREAD 4.8
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • Code evidence confirms a real, reachable logic gap (missing action.kind check after type check), but exploit maturity is conceptual, auth barrier is strong (requires control of the signing process), and network exposure is internal. This does not meet HIGH criteria (no confirmed exploit maturity beyond conceptual, no EPSS/CVE, requires a non-trivial precondition of agent compromise), so medium is appropriate and consistent with the scanner's original rating.
  • Composite score: 5.3
  • Environment: production

Summary: The disclosure step-up verifier checks a top-level type discriminator to avoid reading foreign authorization-contexts as disclosures, but never validates the nested action.kind field, leaving a narrower semantic type-confusion gap where a correctly-typed context could carry a mismatched action kind.

📝 Description:

A holder relying on this library's output to render a consent prompt could be shown disclosure-flavoured language and claimTypes for an action that is not actually a disclosure, undermining accurate informed consent in the approval UX.

🧪 Proof of Concept:

Only ctx.type is checked before proceeding to trust action.previewId/claimTypes; action.kind, the field the comment says is the per-action discriminator, is read nowhere in this function.

  const ctx = (payload.ext?.[AUTHZ_CONTEXT_EXT_KEY] ?? {}) as Record<string, unknown>;

  if (ctx.type !== DISCLOSURE_AUTHZ_CONTEXT_TYPE) {
    return {
      ok: false,
      reason: `authorization context is ${String(ctx.type)}, not a disclosure`,
    };
  }

  // The specifics live under `action`, keyed by `kind` — the shape every
  // authorization context uses, so one renderer serves all of them.
  const action = (ctx.action ?? {}) as Record<string, unknown>;

  if (action.previewId !== refusal.previewId) {
    return {
      ok: false,
      reason:
  ...
  const claimTypes = Array.isArray(action.claimTypes)
    ? action.claimTypes.filter((t): t is string => typeof t === "string")
    : [];

Vulnerable lines: 182, 206

🔎 Evidence: packages/core/src/persona/step-up.ts:185

  if (ctx.type !== DISCLOSURE_AUTHZ_CONTEXT_TYPE) {
    return { ok: false, reason: `authorization context is ${String(ctx.type)}, not a disclosure` };
  }
  const action = (ctx.action ?? {}) as Record<string, unknown>;
  if (action.previewId !== refusal.previewId) {

💥 Impact:

A holder relying on this library's output to render a consent prompt could be shown disclosure-flavoured language and claimTypes for an action that is not actually a disclosure, undermining accurate informed consent in the approval UX.

Confidentiality: low — no additional data is disclosed beyond what the actual signed action already grants at the field level · Integrity: medium — the semantic meaning presented to the holder/relying UI may not match the actual operation approved, undermining informed consent · Availability: none

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: strong
  • Attack path: EP-001 verifyDisclosureStepUp(seen, enrolled) → payload.ext[AUTHZ_CONTEXT_EXT_KEY] read → ctx.type check (line 185) passes → action.kind never validated → action.previewId/claimTypes read (line 189-206) → returned as ok:true context

⚖️ Triage Factors:

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

Attack scenario: A compromised or buggy agent signs an authorization-context with the correct disclosure type but a mismatched action.kind, causing the verifier to approve/label a non-disclosure action as a disclosure.

🔧 Remediation:

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

Adds an explicit check that action.kind === 'disclose', enforcing the same discriminator discipline at the nested level that was already applied at the top level, closing the semantic type-confusion gap.

Vulnerable code:

  if (ctx.type !== DISCLOSURE_AUTHZ_CONTEXT_TYPE) {
    return { ok: false, reason: `authorization context is ${String(ctx.type)}, not a disclosure` };
  }
  const action = (ctx.action ?? {}) as Record<string, unknown>;

Secure code:

  if (ctx.type !== DISCLOSURE_AUTHZ_CONTEXT_TYPE) {
    return { ok: false, reason: `authorization context is ${String(ctx.type)}, not a disclosure` };
  }
  const action = (ctx.action ?? {}) as Record<string, unknown>;
  if (action.kind !== "disclose") {
    return { ok: false, reason: `authorization context action is ${String(action.kind)}, not a disclose action` };
  }

Additional recommendations:

  • Adopt a runtime schema validator (e.g. zod) with a discriminated union keyed by both type and action.kind so mismatches are impossible to construct.
  • Add a unit test asserting action.kind mismatches are rejected even when ctx.type matches, mirroring the existing 'not a disclosure' test.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: In packages/core/src/persona/step-up.ts, verifyDisclosureStepUp gates on if (ctx.type !== DISCLOSURE_AUTHZ_CONTEXT_TYPE) { return { ok:false, ... } } and then does const action = (ctx.action ?? {}) as Record<string, unknown>; followed by if (action.previewId !== refusal.previewId) {...} and later reads action.claimTypes, action.verifierDid, action.purpose directly — nowhere is action.kind (e.g. === 'disclose') checked, so the shape of action is never confirmed to be a disclosure action beyond the outer type string matching. EVIDENCE NOT FOUND: No validation of action.kind anywhere in the function or in verifyStepUpApproveRequest (not shown, but the disclosure-specific logic is entirely in this file). No schema/zod validation of the action object shape. CHANGED VS PRE-EXISTING: This is CHANGED code — the diff hunk shows this exact ctx.type check and action object were introduced by this MR (previously the code checked ctx.previewId directly with no type/action split at all: '- if (ctx.previewId !== refusal.previewId) {' replaced by the new type+action gating). VERDICT JUSTIFICATION: The finding accurately identifies that the newly introduced action sub-object's own kind discriminator is never validated, meaning any action shape (as long as ctx.type matches) is accepted and its previewId/claimTypes/verifierDid/purpose fields are trusted and surfaced to the holder — a genuine, if narrow, semantic gap introduced by this MR's refactor.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 ctx.summary read from unsigned-adjacent path independent of action-level type gate

Field Detail
Severity MEDIUM
Location packages/core/src/persona/step-up.ts:210
Finding ID github_pr-952ffaef230b
CWE CWE-345, CWE-703
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1565.001: Stored Data Manipulation
CAPEC CAPEC-384: Application API Message Manipulation via Man-in-the-Middle
DREAD 3.2
Reachability ⚪ Not reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — Scanner reachability analysis explicitly confirms the vulnerable line (214) is unreachable for non-matching context types because an earlier gate at line 185 returns ok:false first. Exploitability is rated 'none' by the scanner and exploit maturity is 'theoretical' (derived from code complexity, not any real PoC). Auth barrier is strong and exposure is internal. This matches the Low severity band: no confirmed exploit, not currently reachable, requires a hypothetical future code change to become exploitable.
  • Composite score: 4.8
  • Environment: production

Summary: context.summary is populated from the top-level ctx.summary field rather than from the type-gated action object, which is safe today only because of the early-return type check earlier in the function — a structurally fragile pattern flagged by the threat model as a latent spoofing risk.

📝 Description:

No current exploitable impact given existing control flow; if reintroduced via refactor, could let a non-disclosure context's summary text be shown to a holder as disclosure-approval language, undermining informed consent.

🧪 Proof of Concept:

summary is the only field in the returned context sourced from the outer ctx object rather than the type-gated action object, breaking the 'one shape every context uses' discipline applied to the rest of the fields.

  const claimTypes = Array.isArray(action.claimTypes)
    ? action.claimTypes.filter((t): t is string => typeof t === "string")
    : [];

  return {
    ok: true,
    issuer: verified.issuer,
    context: {
      claimTypes,
      ...(typeof ctx.summary === "string" ? { summary: ctx.summary } : {}),
      ...(typeof action.verifierDid === "string" ? { verifierDid: action.verifierDid } : {}),
      ...(typeof action.purpose === "string" ? { purpose: action.purpose } : {}),
    },
  };
}

Vulnerable lines: 205, 217

🔎 Evidence: packages/core/src/persona/step-up.ts:210

    context: {
      claimTypes,
      ...(typeof ctx.summary === "string" ? { summary: ctx.summary } : {}),
      ...(typeof action.verifierDid === "string" ? { verifierDid: action.verifierDid } : {}),

💥 Impact:

No current exploitable impact given existing control flow; if reintroduced via refactor, could let a non-disclosure context's summary text be shown to a holder as disclosure-approval language, undermining informed consent.

Confidentiality: none · Integrity: low — potential for holder-facing summary text to diverge from the actual approved action fields if code evolves · Availability: none

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: strong
  • Attack path: EP-001 verifyDisclosureStepUp → ctx.type gate at line 185 already returns ok:false for non-matching types before line 214 is reached in the current code; not currently exploitable end-to-end, but structurally fragile

⚖️ Triage Factors:

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

Attack scenario: If the type-gate ordering or early-return logic in verifyDisclosureStepUp is ever refactored, ctx.summary could become readable for a non-disclosure context type, letting a foreign action's summary be shown as if it were a disclosure summary.

🔧 Remediation:

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

Prefers action.summary (inside the type-gated, kind-specific object) when present, falling back to the top-level ctx.summary only as a legacy/compat path, keeping all rendered fields consistently sourced from the validated action shape.

Vulnerable code:

    context: {
      claimTypes,
      ...(typeof ctx.summary === "string" ? { summary: ctx.summary } : {}),
      ...(typeof action.verifierDid === "string" ? { verifierDid: action.verifierDid } : {}),
      ...(typeof action.purpose === "string" ? { purpose: action.purpose } : {}),
    },

Secure code:

    context: {
      claimTypes,
      ...(typeof action.summary === "string"
        ? { summary: action.summary }
        : typeof ctx.summary === "string"
          ? { summary: ctx.summary }
          : {}),
      ...(typeof action.verifierDid === "string" ? { verifierDid: action.verifierDid } : {}),
      ...(typeof action.purpose === "string" ? { purpose: action.purpose } : {}),
    },

Additional recommendations:

  • Document explicitly in code comments why summary is intentionally read from the outer ctx object (if that is an intended design decision) to prevent future maintainers from assuming it follows the same trust boundary as action fields.
  • Add a regression test that moves/duplicates the type check or refactors control flow to confirm summary still cannot leak from a foreign-typed context.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: The returned context object is built as context: { claimTypes, ...(typeof ctx.summary === "string" ? { summary: ctx.summary } : {}), ...(typeof action.verifierDid === "string" ? {...} : {}), ...(typeof action.purpose === "string" ? {...} : {}) } — note ctx.summary is read from the top-level ctx object (outside the action sub-object), while verifierDid/purpose/claimTypes/previewId are all read from the type-gated action sub-object. This is an inconsistency: everything else that is rendered to the holder comes from action, but summary comes from the shared, less-scoped ctx level. EVIDENCE NOT FOUND: No additional validation ties ctx.summary to the action.kind==='disclose' gate (which itself doesn't exist per the sibling finding). CHANGED VS PRE-EXISTING: CHANGED — the diff shows summary extraction is newly added in this MR: '+ ...(typeof ctx.summary === "string" ? { summary: ctx.summary } : {}),' replacing the old ctx.verifierDid/ctx.purpose reads, confirming this is new/modified code in step-up.ts. VERDICT JUSTIFICATION: Since ctx.type is checked but action.kind is not (per the sibling finding), and summary is read independent of the action-level gate, a same-ext-key context of a different action kind could still supply a top-level summary string that gets surfaced as if it belonged to the disclosure action — a real, if low-severity, inconsistency introduced by this MR's refactor.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.


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

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

🛡️ Threat Model & Affect Analysis — PR #190

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/persona-step-up-context-shapemain
Generated 2026-09-08

ℹ️ 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

Fixes a type-confusion vulnerability in the persona disclosure step-up verification by adding a 'type' discriminator check and reading operation-specific fields (previewId, verifierDid, claimTypes, purpose) from a nested 'action' object instead of the shared top-level 'ext' context. This prevents a Cierge share-ask authorization context (which shares the same reverse-DNS ext key) from being misread as a Persona disclosure approval and shown to the holder with disclosure-specific language.

Diff: +62 / -15 lines
Types: security, bugfix, test

Risk Assessment

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

This is a security-hardening fix (closes a real type-confusion vulnerability, CWE-843) with good test coverage including a targeted adversarial regression test. Risk is elevated to medium rather than low because: (1) the fix is incomplete — action.kind is never validated, leaving a narrower residual type-confusion gap within the same declared type; (2) the schema change is breaking for any payload producer not included in this diff, creating an operational/availability risk if not coordinated; (3) the new summary field is inconsistently scoped relative to the rest of the fix. No new external attack surface or entry point is introduced, and the change strictly narrows/adds validation rather than removing controls, which limits the ceiling of the risk.

Review Focus Areas:

  • Verify all upstream producers of the signed approve-request payload have been updated to the new nested action schema (breaking change risk)
  • Confirm action.kind is validated in a follow-up, since the current fix only checks the top-level type string
  • Confirm the Cierge (or other) consumer of the shared ext key independently applies its own type discriminator check

⚠️ Security Implications

🟡 Type-confusion vulnerability fixed via context 'type' discriminator

Type-confusion vulnerability fixed via context 'type' discriminator

Action: Confirmed present in this diff. Additionally validate action.kind === "disclose" as a second-layer check, since the current fix only gates on the top-level type string, not the nested action.kind.

🔵 action.kind is read from payload but never validated

action.kind is read from payload but never validated

Action: Add an explicit if (action.kind !== "disclose") return { ok: false, reason: ... } check immediately after the type check.

🟡 Authorization-context wire schema is a breaking change for existing producers

Authorization-context wire schema is a breaking change for existing producers

Action: Coordinate deployment with all payload producers; consider a transition period accepting both schemas with a deprecation warning, or verify (via repo-wide search) that this diff already includes all producers.

🔵 'summary' field sourced from top-level context, inconsistent with action-scoping

'summary' field sourced from top-level context, inconsistent with action-scoping

Action: Move summary extraction under the same type-gated scope as action fields for consistency, or explicitly document why it is intentionally top-level (e.g. shared across all context types).

🧩 Affected Components

Component Impact Change What Changed
persona/step-up disclosure verification high modified Added a type discriminator check on the shared authorization-context ext payload and restructured field extraction to read operation-specifi
Shared 'org.openvtc.authorization-context' ext-key namespace (cross-feature, includes Cierge) medium modified This module now enforces a type discriminator on its consumption of the shared ext key, but other consumers of the same key (e.g. Cierge sha
persona.step-up test suite low modified Test fixtures updated to the new nested schema (type, action.*), and a new adversarial regression test added asserting that a Cierge-shaped

📁 File Classifications

packages/core/src/persona/step-up.ts

  • Type: security

💡 Recommendations

  • MUST — Add explicit validation that action.kind === "disclose" before extracting any action fields (effort: small)
    • Closes the residual type-confusion gap where a payload with the correct top-level type but wrong/missing action.kind would still be processed as a disclosure.
  • MUST — Confirm and update all upstream producers of the signed authorization-context payload to the new nested action schema before merging/deploying (effort: medium)
    • This is a breaking wire-format change; unmigrated producers will have all approvals rejected, causing a functional outage of the disclosure step-up flow.
  • SHOULD — Add structured audit logging on all ok:false rejection paths (type mismatch, previewId mismatch) (effort: small)
    • Currently no logging exists for rejected step-up attempts, hindering detection of probing/confusion attempts against the shared ext-key channel.
  • SHOULD — Move summary extraction to be scoped consistently with the other action fields (post type-check, ideally under action) (effort: small)
    • Keeps the new field's trust boundary consistent with the design intent of the rest of this fix.
  • SHOULD — Audit all other consumers of the shared 'org.openvtc.authorization-context' ext key (e.g. Cierge) to confirm each enforces its own type discriminator (effort: medium)
    • The vulnerability class fixed here is inherent to the shared-key design and could recur in any other consumer that hasn't been similarly hardened.

🛡️ STRIDE Threat Model

Identified Threats (11)

🟡 : Untitled Threat

Field Detail
Category Spoofing
Severity Medium
Likelihood Possible

Mitigation: Move summary extraction under the type-gated action object, or re-derive it strictly from the already-verified refusal.reason/request.reason rather than from attacker/agent-supplied ctx.summary.


🟡 : Untitled Threat

Field Detail
Category Spoofing
Severity Medium
Likelihood Possible

Mitigation: Explicitly validate action.kind === "disclose" in addition to the top-level type check, rejecting any other kind even when type matches.


🔵 : Untitled Threat

Field Detail
Category Tampering
Severity Low
Likelihood Unlikely

Mitigation: Use an explicit allow-list constructor for context instead of spread-merging attacker-influenced keys, and reject reserved keys like __proto__.


🔵 : Untitled Threat

Field Detail
Category Tampering
Severity Low
Likelihood Possible

Mitigation: Validate claimTypes against an enumerated allow-list of known claim type URIs before returning them for display.


🔵 : Untitled Threat

Field Detail
Category Information Disclosure
Severity Low
Likelihood Likely

Mitigation: Sanitize or whitelist the value before interpolation into user-facing/logged error reasons; avoid reflecting arbitrary attacker-controlled types verbatim.


🔵 : Untitled Threat

Field Detail
Category Repudiation
Severity Low
Likelihood Likely

Mitigation: Add structured audit logging on every ok:false rejection path, including the offending type/previewId values and verified issuer DID.


🟡 : Untitled Threat

Field Detail
Category Elevation of Privilege
Severity Medium
Likelihood Possible

Mitigation: Require and validate action.kind === "disclose" as a hard precondition before any field extraction from action.


🟡 : Untitled Threat

Field Detail
Category Tampering
Severity Medium
Likelihood Possible

Mitigation: Add operation-scoped authorization checks (e.g., per-operation capability/permission on the executor DID) in addition to the enrollment list.


🔵 : Untitled Threat

Field Detail
Category Denial of Service
Severity Low
Likelihood Unlikely

Mitigation: Cap the maximum number of claimTypes processed/rendered (e.g., truncate or reject arrays beyond a sane limit).


🔵 : Untitled Threat

Field Detail
Category Spoofing
Severity Low
Likelihood Unlikely

Mitigation: Consider per-feature ext keys instead of a shared key with a type discriminator, or add a version/schema registry with strict validation at ingestion.


🟡 : Untitled Threat

Field Detail
Category Tampering
Severity Medium
Likelihood Possible

Mitigation: Document and enforce that action/ctx must only ever be read from the cryptographically verified payload object, never from cached/derived copies, and add integration tests covering UI-layer consumption paths.



🍝 PASTA Threat Model

Objectives

Technical Scope

Entry Points (1): EP-001 FUNCTION_CALL verifyDisclosureStepUp(seen, enrolled)

Trust Boundaries (3): TB-1 Client → HTTP Service (untrusted user input) · TB-2 Service → Database (query construction boundary) · TB-3 Service → External APIs (SSRF boundary)

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

Use Cases (5)

  • Data Flow 1:
  • Data Flow 2:
  • Data Flow 3:
  • Data Flow 4:
  • Data Flow 5:

⚔️ Attack Scenarios (3)

Exfiltrate or corrupt sensitive data

flowchart LR
  S0["Step 1: undefined"]
  S1["Step 2: undefined"]
  S2["Step 3: undefined"]
  S3["Step 4: undefined"]
  S4["Step 5: undefined"]
  S0 --> S1
  S1 --> S2
  S2 --> S3
  S3 --> S4
Loading

Escalate privileges and assume unauthorized identity

flowchart LR
  S0["Step 1: undefined"]
  S1["Step 2: undefined"]
  S2["Step 3: undefined"]
  S3["Step 4: undefined"]
  S0 --> S1
  S1 --> S2
  S2 --> S3
Loading

Disrupt service availability and hide evidence

flowchart LR
  S0["Step 1: undefined"]
  S1["Step 2: undefined"]
  S2["Step 3: Chain denial of service + repudiation weaknesses to achieve disrupt service availability and hide evidence"]
  S0 --> S1
  S1 --> S2
Loading

🛡️ Recommended Countermeasures

  • CM-001 (Immediate): Implement robust authentication controls including multi-factor authentication, secure session handling with proper token rotation, and anti-replay mechanisms. Validate all authentication tokens server-side and enforce session timeouts.
    • Effort: days
    • Mitigates: Spoofing, A07, undefined, undefined, undefined
  • CM-002 (Immediate): Apply strict input validation at all trust boundaries using allowlists. Parameterize all database queries, validate request payloads against schemas, and implement integrity checks for sensitive data modifications.
    • Effort: days
    • Mitigates: Tampering, Injection, A03, undefined, undefined, undefined
  • CM-003 (Short-term): Implement tamper-evident structured logging for all security-relevant events including authentication attempts, authorization decisions, and data access. Forward logs to a centralized SIEM with integrity protection.
    • Effort: days
    • Mitigates: Repudiation, A09, undefined
  • CM-004 (Immediate): Enforce least-privilege access controls at every endpoint. Remove sensitive data from error responses, implement proper object-level authorization, and encrypt sensitive data at rest and in transit.
    • Effort: days
    • Mitigates: Information Disclosure, A01, IDOR, undefined
  • CM-005 (Short-term): Apply rate limiting at route and user level. Set request size limits, implement timeouts for all external calls, use circuit breakers for downstream services, and validate algorithmic complexity of user inputs.
    • Effort: days
    • Mitigates: Denial of Service, DoS, A04, undefined
  • CM-006 (Immediate): Implement centralized authorization middleware with RBAC/ABAC. Enforce object-level and function-level access checks on every endpoint. Apply principle of least privilege to service accounts and API keys.
    • Effort: days
    • Mitigates: Elevation of Privilege, A01, undefined

📊 Risk Summary

Total Threats: 11

By Severity: Low: 6 · Medium: 5

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

🛡️ Risk Mitigation Strategy

  • Countermeasures: {"id":"CM-001","name":"Strengthen authentication and session management","effort":"days","priority":"Immediate","mitigates":["Spoofing","A07","undefined","undefined","undefined"],"description":"Implement robust authentication controls including multi-factor authentication, secure session handling with proper token rotation, and anti-replay mechanisms. Validate all authentication tokens server-side and enforce session timeouts.","breaks_story":"Mitigates 3 Spoofing threats by removing the preconditions required for exploitation.","effectiveness":"medium"}; {"id":"CM-002","name":"Input validation and data integrity controls","effort":"days","priority":"Immediate","mitigates":["Tampering","Injection","A03","undefined","undefined","undefined"],"description":"Apply strict input validation at all trust boundaries using allowlists. Parameterize all database queries, validate request payloads against schemas, and implement integrity checks for sensitive data modifications.","breaks_story":"Mitigates 4 Tampering threats by removing the preconditions required for exploitation.","effectiveness":"medium"}; {"id":"CM-003","name":"Comprehensive audit logging and monitoring","effort":"days","priority":"Short-term","mitigates":["Repudiation","A09","undefined"],"description":"Implement tamper-evident structured logging for all security-relevant events including authentication attempts, authorization decisions, and data access. Forward logs to a centralized SIEM with integrity protection.","breaks_story":"Mitigates 1 Repudiation threat by removing the preconditions required for exploitation.","effectiveness":"medium"}; {"id":"CM-004","name":"Data exposure prevention and access controls","effort":"days","priority":"Immediate","mitigates":["Information Disclosure","A01","IDOR","undefined"],"description":"Enforce least-privilege access controls at every endpoint. Remove sensitive data from error responses, implement proper object-level authorization, and encrypt sensitive data at rest and in transit.","breaks_story":"Mitigates 1 Information Disclosure threat by removing the preconditions required for exploitation.","effectiveness":"medium"}; {"id":"CM-005","name":"Rate limiting and resource exhaustion guards","effort":"days","priority":"Short-term","mitigates":["Denial of Service","DoS","A04","undefined"],"description":"Apply rate limiting at route and user level. Set request size limits, implement timeouts for all external calls, use circuit breakers for downstream services, and validate algorithmic complexity of user inputs.","breaks_story":"Mitigates 1 Denial of Service threat by removing the preconditions required for exploitation.","effectiveness":"medium"}; {"id":"CM-006","name":"Authorization enforcement and privilege separation","effort":"days","priority":"Immediate","mitigates":["Elevation of Privilege","A01","undefined"],"description":"Implement centralized authorization middleware with RBAC/ABAC. Enforce object-level and function-level access checks on every endpoint. Apply principle of least privilege to service accounts and API keys.","breaks_story":"Mitigates 1 Elevation of Privilege threat by removing the preconditions required for exploitation.","effectiveness":"medium"}
  • Priority Actions: Strengthen authentication and session management; Input validation and data integrity controls; Data exposure prevention and access controls; Authorization enforcement and privilege separation

Generated by Agentic Sec — Threat Model & Affect Analysis Agent

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

Confirmed (2)

  • 🟡 action.kind is never validated to equal 'disclose' after type gate — potential semantic type confusion
  • 🟡 ctx.summary read from unsigned-adjacent path independent of action-level type gate (triaged LOW→MEDIUM)

Security review on #190 found the gap, correctly: the module's own comment
names `kind` as the per-action discriminator and nothing checked it.

`type` and `kind` answer different questions — which producer's vocabulary the
context speaks, and which action within it. Checking one and not the other
leaves the narrower confusion open. It rejects nothing today, because the
persona context type carries only `disclose`; it is here for the second `kind`
added under this type, which would otherwise be read as a disclosure, have its
fields mined for claimTypes it never had, and be shown to the holder in a
disclosure's words.

Absence is not permission either — an action with no `kind` is refused, which
is the shape a producer that forgot the discriminator emits.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78

Copy link
Copy Markdown
Contributor Author

Thanks — one of the two is a real gap and is now fixed; the other I'm declining, with reasons.

Fixed: action.kind never validated (github_pr-bcd3292ab67b)

Correct, and the sharpest part of the finding is that the module's own comment names kind as the per-action discriminator and then nothing checks it. Pushed in 4a31167.

I'd frame the risk differently from the report, though. The stated scenario — "a compromised or buggy agent signs a correctly-typed context with a mismatched action.kind" — is weak on the compromised half: an agent that can sign anything can just sign a fully correct disclosure context and lie about the claim types. Signing authority is the game there, not this check.

Where it genuinely bites is the buggy/evolved half: the second kind added under https://openvtc.org/persona/authorization-context/0.1. That one would be read as a disclosure, have its fields mined for claimTypes it never had, and be shown to the holder in a disclosure's words — with no attacker anywhere. That is the case the check is for, and it's why the fix also refuses an action with no kind at all (the shape a producer that forgot the discriminator emits — absence is not permission).

Two tests added, so it's 12 in persona.step-up.mjs now.

Declined from the same finding: the zod / discriminated-union recommendation. This package has no runtime schema validator and adding one to packages/core is a dependency and a pattern for the whole layered library, not a fix for this function. Worth its own discussion if the codebase wants it generally.

Declined: ctx.summary "read from unsigned-adjacent path" (github_pr-952ffaef230b)

Declining this one on the facts.

There is no unsigned path here. ctx is document.payload.ext["org.openvtc.authorization-context"]inside the Trust-Task document whose Data Integrity proof (eddsa-jcs-2022, assertionMethod) verifyStepUpApproveRequest verified two statements earlier, and whose signer it required to be an enrolled executor. ctx.summary is exactly as signed as action.claimTypes; there is no integrity difference between the two reads. The existing test a tampered context does not survive the proof demonstrates it — appending a claim type after signing fails verification.

Top-level summary is the shared convention, not a deviation from it. The shape every authorization context uses is {type, summary, risk, action}: summary is the context's one-line account of the act and action is the per-kind detail. The Cierge share ask has it at top level, and the agent's reason_and_context reads ctx.summary back out as the request's reason. Moving summary under action would put this repo out of step with both, for no integrity gain.

The report also grades it Not reachable / exploitability none / theoretical, with the impact conditional on "if reintroduced via refactor". I'd rather leave the code matching the shared shape than restructure it against a hypothetical future edit.

Happy to reopen if I've misread the concern — in particular if the claim is that payload.ext is excluded from the proof somewhere, that would be a serious bug and I'd want to know.

@stormer78
stormer78 merged commit e0689f7 into main Sep 8, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/persona-step-up-context-shape branch September 8, 2026 05:33
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