Static read-through audit of crates/dpp-resolver. HTML escaping, JWS verification, caching, and rate limiting are all sound. Two verified findings, both on the same public GTIN-resolution handler.
1. Unvalidated gtin path parameter used in a server-to-server request
Location: src/handlers/resolve_by_gtin.rs:32,147
Issue: format!("{}/public/dpp/by-gtin/{gtin}", state.vault_base_url) — unlike dpp_id, which is validated via is_valid_dpp_id in every other handler specifically to close "a latent SSRF / cache-key / XSS surface" (per src/domain/id.rs:5-8), no equivalent check exists for gtin.
Failure scenario: Axum decodes percent-encoded path segments after routing, so a request to /01/%2E%2E%2Fadmin yields gtin = "../admin", which URL dot-segment normalization collapses against by-gtin/, redirecting the outbound request to a different path on the internal vault host (e.g. {vault_base_url}/public/admin) — a path-traversal/SSRF pivot. The crate's own test suite explicitly models this exact attack shape ("../admin") for dpp_id, confirming it's a known-and-handled category simply missed for gtin.
2. Redirect host trusts a field the codebase itself documents as tamperable
Location: src/handlers/resolve_by_gtin.rs:55-61
Issue: base_url (used for both the 307 redirect Location and linkset URLs) is taken verbatim from passport.get("qrCodeUrl") with no allow-list check against the expected resolver host. qrCodeUrl is a documented mutable field (did.rs MUTABLE_FIELDS test) explicitly called out elsewhere in this crate as "content-binding-exempt and tamperable (red-team RT2-2)" — which is why the sibling resolve_qr_handler deliberately hardcodes https://passport.odal-node.io/dpp/{dpp_id} instead of trusting it.
Failure scenario: If qrCodeUrl is altered post-signature (which the crate's own docs say it can be), a GS1 Digital Link scan (/01/{gtin}) 307-redirects to an attacker-chosen host — an open redirect on exactly the path the QR-code path was specifically hardened against.
Static read-through audit of
crates/dpp-resolver. HTML escaping, JWS verification, caching, and rate limiting are all sound. Two verified findings, both on the same public GTIN-resolution handler.1. Unvalidated
gtinpath parameter used in a server-to-server requestLocation:
src/handlers/resolve_by_gtin.rs:32,147Issue:
format!("{}/public/dpp/by-gtin/{gtin}", state.vault_base_url)— unlikedpp_id, which is validated viais_valid_dpp_idin every other handler specifically to close "a latent SSRF / cache-key / XSS surface" (persrc/domain/id.rs:5-8), no equivalent check exists forgtin.Failure scenario: Axum decodes percent-encoded path segments after routing, so a request to
/01/%2E%2E%2Fadminyieldsgtin = "../admin", which URL dot-segment normalization collapses againstby-gtin/, redirecting the outbound request to a different path on the internal vault host (e.g.{vault_base_url}/public/admin) — a path-traversal/SSRF pivot. The crate's own test suite explicitly models this exact attack shape ("../admin") fordpp_id, confirming it's a known-and-handled category simply missed forgtin.2. Redirect host trusts a field the codebase itself documents as tamperable
Location:
src/handlers/resolve_by_gtin.rs:55-61Issue:
base_url(used for both the 307 redirectLocationand linkset URLs) is taken verbatim frompassport.get("qrCodeUrl")with no allow-list check against the expected resolver host.qrCodeUrlis a documented mutable field (did.rsMUTABLE_FIELDStest) explicitly called out elsewhere in this crate as "content-binding-exempt and tamperable (red-team RT2-2)" — which is why the siblingresolve_qr_handlerdeliberately hardcodeshttps://passport.odal-node.io/dpp/{dpp_id}instead of trusting it.Failure scenario: If
qrCodeUrlis altered post-signature (which the crate's own docs say it can be), a GS1 Digital Link scan (/01/{gtin}) 307-redirects to an attacker-chosen host — an open redirect on exactly the path the QR-code path was specifically hardened against.