Skip to content

dpp-domain: patch_fields bypasses passport state machine; redact() fails open and leaks confidential data #24

Description

@LKSNDRTMLKV

Static read-through audit of crates/dpp-domain. Five verified findings — the most severe batch in dpp-core.

1. patch_fields() allows arbitrary JSON deltas to bypass the state machine

Location: src/ports/passport_repo.rs:88 (default impl, lines 76-93)
Issue: The default patch_fields() merges an arbitrary serde_json::Value delta directly onto the serialized Passport (pm.extend(dm)) with no field allowlist, then persists it without going through PassportStatus::transition_to().
Failure scenario: A delta like {"status":"active","retentionLocked":false,"jwsSignature":"x"} bypasses the state machine entirely — it can flip a retention-locked passport back to unlocked or forge signature fields. Any adapter that doesn't override this default inherits the bypass.

2. redact() fails open and leaks confidential-tier data

Location: src/domain/passport/passport.rs:299-303
Issue: When catalog.get(key) returns None for the passport's sector, sector_data is serialized and returned unredacted. The doc comment acknowledges this ("fail-open at the domain layer") and defers enforcement to a caller-side "vault layer," but nothing in this crate enforces that such a caller always exists.
Failure scenario: Any code calling Passport::redact() directly for a sector absent from its catalog instance leaks confidential-tier sector fields to a public-tier viewer.

3. Schema-version format check is vacuously true on malformed input

Location: src/domain/passport/passport.rs:166-171
Issue: The semver check for schema_version verifies parts 0/1 are digits and part 2 is non-empty, but never checks part 2 is digits. .chars().all(...) on an empty string is vacuously true.
Failure scenario: schema_version = ".5.0" (empty major) or "1.0.abc" (non-numeric patch) both pass as "valid," later failing semver::Version parsing elsewhere and silently skipping schema validation (see finding 5).

4. Catalog registration doesn't validate its own documented invariant

Location: src/catalog/catalog.rs:161-167
Issue: SectorCatalog::register() only checks key uniqueness — never that current_schema_version is valid semver or a member of schema_versions, despite the doc requiring it.
Failure scenario: A caller loading descriptors from external JSON can register a sector with an unparseable version, which then silently skips JSON-Schema validation for every passport in that sector (see finding 5).

5. Runtime-registered schemas are invisible to validation

Location: src/domain/validation/functions.rs:19-28, mod.rs:3-4
Issue: Schema resolution goes through private OnceLock statics seeded once from embedded manifests, contradicting the doc's claim of routing through the shared VersionedSchemaRegistry.
Failure scenario: Anything registered at runtime via VersionedSchemaRegistry::register() (the documented hot-reload workflow) is never seen by Passport::validate(), which always validates against the stale embedded schema — and (per finding 3/4) an unparseable version silently skips validation entirely rather than erroring.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcomplianceRegulatory/compliance correctness issuesecuritySecurity-relevant issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions