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.)
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:
gtinis 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 whoseqrCodeUrlmerely 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 thestatusscalar column — notsector,retention_locked,schema_version,published_at, even though the JSONBdocis updated.Failure scenario: Patching
retentionLockedtotruethrough this path updates the doc, but theretention_lockedcolumn (which backs the actual DB-level retention-lock enforcement, perpg/mod.rs:41-43) staysfalse— 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 aregistry_syncrow exists (e.g.rejectedafter the EU registry declines it), republishing with a corrected payload silently does nothing.due()only selectsstatus='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_idsilently returnsOk(())— 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_scopesmaps a NULL/emptyscopescolumn toAdmin("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.)