Summary
PIA's OIDC issuer allowlist for Jenkins tokens uses a bare string-prefix check (issuer.startswith('https://ci.eclipse.org') in is_issuer_known, pia/models.py:139) instead of validating the issuer as a properly host-bounded URL. An attacker can craft an issuer such as https://ci.eclipse.org@evil.host (userinfo trick) or https://ci.eclipse.org.evil.host (suffix trick) that satisfies the prefix check while pointing the OIDC discovery and JWKS fetches at a server the attacker controls. An unauthenticated caller of POST /v1/upload/sbom can use this to force PIA to make outbound HTTP(S) requests to an arbitrary attacker-chosen host, and to have oidc.verify_token accept a JWT signed with the attacker's own key.
Impact
An unauthenticated remote attacker can trigger blind server-side requests (SSRF) from PIA to any host of their choosing — including cloud metadata endpoints and internal-only services — by supplying a crafted iss claim in an unverified bearer token. PIA's fetches to {issuer}/.well-known/openid-configuration and the resulting jwks_uri (pia/oidc.py:27,31,51-52) happen before any signature check or workload lookup, so no registered workload or valid credential is required to trigger the outbound request. Separately, because the fetched JWKS is entirely attacker-supplied, verify_token will accept a token signed with the attacker's own key and return attacker-chosen claims, subverting the intended OIDC signature trust root.
This does not by itself complete an authentication bypass: find_workload_by_claims (pia/models.py:170) still requires the verified issuer to exactly equal a Workload.issuer value already registered in PIA's database, so an attacker cannot simultaneously satisfy that exact match while routing the fetch to their own server. The confirmed, disclosable impact is unauthenticated blind SSRF plus erosion of the OIDC verification trust boundary — not a standalone end-to-end auth bypass.
Affected versions
<= 0.3.0 (all released versions through the current HEAD c8f57af; Jenkins OIDC support — and this prefix-match check — has been present since the 0.1.0 initial release)
Patched versions
Not yet patched. No fix_commit is set and main at HEAD c8f57af still contains the vulnerable prefix check in is_issuer_known.
Proof of concept
Reproduced in-container against the vulnerable functions directly:
from pia.models import is_issuer_known
for iss in [
"https://ci.eclipse.org@127.0.0.1:8888", # userinfo trick -> real host is 127.0.0.1
"https://ci.eclipse.org.attacker.example", # suffix trick
]:
print(iss, "->", is_issuer_known(iss))
Output:
https://ci.eclipse.org@127.0.0.1:8888 -> True
https://ci.eclipse.org.attacker.example -> True
Calling oidc.verify_token(token, "https://ci.eclipse.org@127.0.0.1:8888", audience) causes a local TCP listener bound to 127.0.0.1:8888 to receive PIA's outbound connection — confirming the fetch target is attacker-controlled, not ci.eclipse.org. A second PoC serves a self-signed-CA-trusted HTTPS discovery document and JWKS at that address; verify_token then accepts a JWT signed with the attacker's own RSA key and returns the attacker-chosen claims, confirming the OIDC trust root is subverted.
Fix suggestion
Validate the issuer as a parsed URL with an exact host match (e.g. urlparse(issuer).hostname == "ci.eclipse.org", or a suffix check anchored on a dotted domain boundary) in is_issuer_known (pia/models.py:139), rather than a bare str.startswith prefix comparison.
References
Summary
PIA's OIDC issuer allowlist for Jenkins tokens uses a bare string-prefix check (
issuer.startswith('https://ci.eclipse.org')inis_issuer_known,pia/models.py:139) instead of validating the issuer as a properly host-bounded URL. An attacker can craft an issuer such ashttps://ci.eclipse.org@evil.host(userinfo trick) orhttps://ci.eclipse.org.evil.host(suffix trick) that satisfies the prefix check while pointing the OIDC discovery and JWKS fetches at a server the attacker controls. An unauthenticated caller ofPOST /v1/upload/sbomcan use this to force PIA to make outbound HTTP(S) requests to an arbitrary attacker-chosen host, and to haveoidc.verify_tokenaccept a JWT signed with the attacker's own key.Impact
An unauthenticated remote attacker can trigger blind server-side requests (SSRF) from PIA to any host of their choosing — including cloud metadata endpoints and internal-only services — by supplying a crafted
issclaim in an unverified bearer token. PIA's fetches to{issuer}/.well-known/openid-configurationand the resultingjwks_uri(pia/oidc.py:27,31,51-52) happen before any signature check or workload lookup, so no registered workload or valid credential is required to trigger the outbound request. Separately, because the fetched JWKS is entirely attacker-supplied,verify_tokenwill accept a token signed with the attacker's own key and return attacker-chosen claims, subverting the intended OIDC signature trust root.This does not by itself complete an authentication bypass:
find_workload_by_claims(pia/models.py:170) still requires the verified issuer to exactly equal aWorkload.issuervalue already registered in PIA's database, so an attacker cannot simultaneously satisfy that exact match while routing the fetch to their own server. The confirmed, disclosable impact is unauthenticated blind SSRF plus erosion of the OIDC verification trust boundary — not a standalone end-to-end auth bypass.Affected versions
<= 0.3.0(all released versions through the current HEADc8f57af; Jenkins OIDC support — and this prefix-match check — has been present since the0.1.0initial release)Patched versions
Not yet patched. No
fix_commitis set andmainat HEADc8f57afstill contains the vulnerable prefix check inis_issuer_known.Proof of concept
Reproduced in-container against the vulnerable functions directly:
Output:
Calling
oidc.verify_token(token, "https://ci.eclipse.org@127.0.0.1:8888", audience)causes a local TCP listener bound to127.0.0.1:8888to receive PIA's outbound connection — confirming the fetch target is attacker-controlled, notci.eclipse.org. A second PoC serves a self-signed-CA-trusted HTTPS discovery document and JWKS at that address;verify_tokenthen accepts a JWT signed with the attacker's own RSA key and returns the attacker-chosen claims, confirming the OIDC trust root is subverted.Fix suggestion
Validate the issuer as a parsed URL with an exact host match (e.g.
urlparse(issuer).hostname == "ci.eclipse.org", or a suffix check anchored on a dotted domain boundary) inis_issuer_known(pia/models.py:139), rather than a barestr.startswithprefix comparison.References