Summary
getDidResolver() defaults webOptions.allowedHttpHosts to ["localhost", "127.0.0.1", "0.0.0.0"], and the web DID resolver sends http:// (not https://) for any host in that list. A default-configured verifier therefore issues plain HTTP requests to its own loopback when resolving attacker-chosen did:web identifiers.
Attack path
JWT verification resolves the iss claim to obtain the verify key before the signature can succeed (did-jwt verifyJWT). isDidWebUri is only a did:web: prefix check, and buildDidPath percent-decodes the method-specific id, so did:web:127.0.0.1%3A6379 resolves to host 127.0.0.1:6379 over http://. There is no scheme allowlist beyond allowedHttpHosts and no private-IP denylist. The same default resolver is what every official example/demo passes into verifyPaymentRequestToken / parseJwtCredential.
Reproduction (source level)
- Attacker signs a JWT with
iss: did:web:127.0.0.1%3A6379.
- Service calls any verify function that defaults to
getDidResolver() (e.g. verifyA2AHandshakeMessage, verifyA2ASignedMessage, verifyPaymentRequestToken).
- The verifier issues
http://127.0.0.1:6379/... on its own machine before any signature check can reject the token. The request repeats per verification attempt.
Note: 169.254.169.254 is not in the HTTP allowlist, so this is loopback/internal-service exposure rather than cloud-metadata SSRF.
Impact
Integrators running unauthenticated internal services on localhost expose them to crafted tokens from any external party: verification triggers attacker-directed internal HTTP requests.
Suggested fix
Default allowedHttpHosts to [] so the convenience wrapper is HTTPS-only, matching getResolver()'s own DEFAULT_ALLOWED_HTTP_HOSTS = []. Callers who truly need plain-HTTP local resolution can opt back in explicitly.
Environment: agentcommercekit/ack at commit b5b25ce (main as of 2026-09-12). Reviewed at source level; no live systems were tested.
Disclosure note: reported to security@agentcommercekit.com per SECURITY.md on 2026-09-14; a maintainer asked for these to be filed publicly, so they are here.
Summary
getDidResolver()defaultswebOptions.allowedHttpHoststo["localhost", "127.0.0.1", "0.0.0.0"], and the web DID resolver sendshttp://(nothttps://) for any host in that list. A default-configured verifier therefore issues plain HTTP requests to its own loopback when resolving attacker-chosendid:webidentifiers.Attack path
JWT verification resolves the
issclaim to obtain the verify key before the signature can succeed (did-jwtverifyJWT).isDidWebUriis only adid:web:prefix check, andbuildDidPathpercent-decodes the method-specific id, sodid:web:127.0.0.1%3A6379resolves to host127.0.0.1:6379overhttp://. There is no scheme allowlist beyondallowedHttpHostsand no private-IP denylist. The same default resolver is what every official example/demo passes intoverifyPaymentRequestToken/parseJwtCredential.Reproduction (source level)
iss: did:web:127.0.0.1%3A6379.getDidResolver()(e.g.verifyA2AHandshakeMessage,verifyA2ASignedMessage,verifyPaymentRequestToken).http://127.0.0.1:6379/...on its own machine before any signature check can reject the token. The request repeats per verification attempt.Note:
169.254.169.254is not in the HTTP allowlist, so this is loopback/internal-service exposure rather than cloud-metadata SSRF.Impact
Integrators running unauthenticated internal services on localhost expose them to crafted tokens from any external party: verification triggers attacker-directed internal HTTP requests.
Suggested fix
Default
allowedHttpHoststo[]so the convenience wrapper is HTTPS-only, matchinggetResolver()'s ownDEFAULT_ALLOWED_HTTP_HOSTS = []. Callers who truly need plain-HTTP local resolution can opt back in explicitly.Environment: agentcommercekit/ack at commit b5b25ce (main as of 2026-09-12). Reviewed at source level; no live systems were tested.
Disclosure note: reported to security@agentcommercekit.com per SECURITY.md on 2026-09-14; a maintainer asked for these to be filed publicly, so they are here.