OX Research disclosed an SSRF vulnerability in Harbor, the CNCF open-source container registry. Any registered user who can create a project can point a webhook at the cloud metadata endpoint and walk away with the server’s IAM credentials. No admin role, no user interaction, and no need to stay online. Set the webhook, wait for a routine image push, and Harbor delivers the request for you.
GitHub Advisory: GHSA-2phj-cp9f-qq6w
CWE: CWE-918 (Server-Side Request Forgery)
CVSS: 6.4 (Moderate)
Affected: Harbor >= 1.7.0, with fixes available in v2.13.6, v2.14.5, v2.15.3, and v2.16.0
Verified on v2.12.0 (official Docker Compose installer). Harbor accepted the report on Sept. 22, 2026, and has released patched versions.
What Harbor Is
Harbor is CNCF’s open-source container registry (29K+ stars, 10M+ pulls on the Core image). It sits between CI/CD, vulnerability scanners, and Kubernetes clusters, with project-level RBAC isolating tenants on a shared instance. That central position is the point: Whatever Harbor’s cloud identity can reach — storage buckets, the K8s API, internal databases — is what an attacker inherits when they steal its credentials.
The Vulnerability
SSRF tricks a server into making requests on the attacker’s behalf. In the cloud, the prize is the Instance Metadata Service at 169.254.169.254. It’s unreachable from the internet, but inside the instance it returns temporary IAM credentials on request. Harbor’s webhook delivery turns the jobservice into an HTTP client that will POST to exactly that address.
Root cause: When a webhook policy is created, validateTargets() only confirms the scheme is http/https and that the URL parses. It never resolves the host or checks for private ranges:

127.0.0.1, 10.0.0.0/8, 172.16/12, 192.168/16, and 169.254.169.254 are all accepted. At delivery time, webhook_job.go passes the stored address straight to http.NewRequest with a plain http.Client. There is no IP filtering anywhere between policy storage and the outbound POST.
Who can do it: The NotificationPolicy:create permission belongs to ProjectAdmin, and any user automatically becomes ProjectAdmin of a project they create. Where self-registration is on, a valid email is enough.
Attack Flow
- Register or log in with any account.
- Create a project, which grants ProjectAdmin automatically.
- Create a webhook targeting http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>. Harbor returns HTTP 201.
- Push any image, or wait for any project event.
- The jobservice fires the webhook and POSTs to IMDS, unvalidated.
- IMDS returns temporary IAM credentials for the Harbor instance role.
- Attacker uses them against cloud resources, from anywhere.
The 201 on step 3 is the whole bug. Harbor accepted a private IP with no error. Step 4 onward runs autonomously with no operator involvement.
Proof of Concept
Tested on Harbor v2.12.0 with the official Docker Compose installer and default configuration. The script starts a listener, creates a webhook targeting it, pushes a minimal OCI image to trigger a PUSH_ARTIFACT event, and captures the outbound POST from Harbor’s jobservice.
The HTTP 201 on webhook creation confirms Harbor accepted a private IP without error. The jobservice then delivered the POST on its own when the push event fired, with no further operator involvement.
POC link forthcoming.
What We Found in the Wild
We scanned every Shodan-indexed Harbor instance on Sept 23, 2026. That’s 6,370 reachable, every one on a pre-fix version.
- 718 are cloud-hosted where IMDS is directly reachable, meaning one webhook away from credential theft.
- 66% expose /api/v2.0/systeminfo unauthenticated, leaking their config.
- 25 have self-registration enabled. On these, no vetted account is needed at all.
- 4 of those 25 resolve to Azure/GCP ranges, so any internet user can register and pull the host’s managed-identity credentials.
The exposed workloads make the blast radius concrete. One Azure instance publicly serves two live LLM inference containers (a llama.cpp deployment and a vLLM multimodal server), both pulled within three days of publication. Credentials there typically authorize GPU compute, model storage, and ML workspaces.
Another Harbor distributes a production service mesh to Kubernetes clusters worldwide, with 560,000+ pulls on a single component. Any authorized user of that registry is therefore one routine push away from reaching internal infrastructure.
Reverse DNS also placed instances at national supercomputing facilities and research universities. Those have no self-registration, but institutional accounts are handed to researchers, collaborators, and guests, so the real attacker pool is large.
Why the Impact Is Worse Than a Normal SSRF
Most SSRFs need admin access to set an outbound URL. This one needs a registered account and one click. And Harbor’s instance role is usually broad: read/write access to the shared image bucket, scanner integration, and replication credentials. Stolen credentials let an attacker act as Harbor’s cloud identity across every service that identity touches. In multi-tenant setups, this bypasses RBAC entirely because cloud credentials sit above Harbor’s project permissions, so Tenant A can read or overwrite Tenant B’s images at the storage layer.
The secondary issue the fix surfaced: Harbor’s jobservice was reflecting the full webhook response body into the project-readable task log. Against IMDS, that body is the AccessKeyId, SecretAccessKey, and SessionToken, written to a log any ProjectAdmin on the project can read. A single malicious webhook doesn’t just hand the attacker credentials. It broadcasts them to the whole project team.
References
- GitHub Advisory: GHSA-2phj-cp9f-qq6w
- CWE-918: https://cwe.mitre.org/data/definitions/918.html
- Harbor webhook docs: https://goharbor.io/docs/latest/working-with-projects/project-configuration/configure-webhooks/
