Skip to content

dpp-dal: LIKE-pattern injection exposes arbitrary passports; stale compliance-critical columns; fail-open scope default #18

Description

@LKSNDRTMLKV

Static read-through audit of crates/dpp-dal. Five verified findings — the most severe batch in dpp-engine alongside dpp-vault.

1. LIKE-pattern injection on a public/consumer-facing lookup

Location: src/pg/repo_passport.rs:141-155 (find_published_by_gtin)
Issue: gtin is bound as a SQL parameter (no SQL injection) but spliced unescaped into a LIKE pattern ('%/01/' || $1 || '/%'), with %/_ metacharacters left uninterpreted.
Failure scenario: A caller passing gtin = "%" turns the pattern into %/01/%/%, matching any active passport whose qrCodeUrl merely contains /01/ anywhere — an attacker can retrieve an arbitrary product passport instead of the one matching their real GTIN. Data exposure on what looks like a public resolver-facing endpoint.

2. Patch path leaves compliance-critical scalar columns stale

Location: src/pg/repo_passport.rs:241-251 (patch_fields)
Issue: Unlike create/update_passport_in_tx, the patch UPDATE only refreshes the status scalar column — not sector, retention_locked, schema_version, published_at, even though the JSONB doc is updated.
Failure scenario: Patching retentionLocked to true through this path updates the doc, but the retention_locked column (which backs the actual DB-level retention-lock enforcement, per pg/mod.rs:41-43) stays false — fail-open on a compliance-critical lock.

3. Rejected registry submissions can never be corrected and resubmitted

Location: src/pg/repo_registry_sync.rs:56-65 (commit_publish)
Issue: INSERT ... ON CONFLICT (passport_id) DO NOTHING — once a registry_sync row exists (e.g. rejected after the EU registry declines it), republishing with a corrected payload silently does nothing. due() only selects status='pending', so it never picks the row back up.
Failure scenario: A passport that was correctly rejected and then fixed has no code path to actually resubmit — contradicting the stated goal of guaranteed registration.

4. Registry-sync status updates don't check whether anything was actually updated

Location: src/pg/repo_registry_sync.rs:107-175 (mark_registered, mark_rejected, mark_attempt_failed)
Issue: None check rows_affected().
Failure scenario: An UPDATE against a non-existent passport_id silently returns Ok(()) — a false-success signal when nothing was actually persisted.

5. Empty/null scopes fail open to Admin

Location: src/pg/repo_api_key.rs:27-35 (key_from_row)
Issue: ApiKeyScope::from_scopes maps a NULL/empty scopes column to Admin ("intentional legacy-compat"), on the auth hot path.
Failure scenario: Any row that ever ends up with empty/null scopes is granted full admin rather than being denied — fail-open where fail-closed is safer. (See also the related, broader finding in dpp-types.)

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