GitLab, webhook SSRF through 0.0.0.0/8
GitLab 19.4.1 UrlBlocker blocks exact 0.0.0.0 and 127.0.0.0/8, not the rest of 0.0.0.0/8. A Maintainer webhook at http://0.0.0.1/ reaches worker loopback with local-requests off. GitLab stores 8 KB of the body. No CVE yet.
- Name
- GitLab, webhook SSRF through 0.0.0.0/8
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- GitLab
- Affected
- GitLab CE through 19.4.1-ce.0 (v19.4.1-ee tree 26212baacadb); unpublished. Project Maintainer, allow_local_requests off. Lab image gitlab/gitlab-ce:19.4.1-ce.0.
- Published
- 01 Oct 2026
- Updated
- 01 Oct 2026
- Tags
- 0-day, gitlab, ssrf, webhook, cwe-918, authenticated
this network is not loopback
The proof of concept is on GitHub: abraxas/gitlab-webhook-0net-ssrf (loopback client; @abraxas_null). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is GitLab CE 19.4.1 (26212baacadb), GitLab Inc. No CVE yet. CWE-918. 8.5 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N). Authenticated Maintainer. Local-requests off. Not unauthenticated. Not IMDS via 169.254.169.254.
Same product, siblings: Gitea HTTPS import pin drop, Gitaly fetch omits resolved_address.
What an attacker can do
POST /api/v4/projects/:id/hooks with url=http://0.0.0.1:<port>/. Trigger or test. Read response_body from the hook events API. On all-in-one omnibus, Sidekiq shares the container netns with Puma, Workhorse, and exporters. Maintainer of their project should not read those.
http://127.0.0.1/ and http://169.254.169.254/ stay blocked. This is the this-network hole.
How I found it
I read url_blocker.rb after the 19.4.1 SSRF fixes. validate_localhost is exact "::" and "0.0.0.0" plus Socket.ip_address_list. validate_loopback is ipv4_loopback? (127.0.0.0/8) and ipv6 loopback. Specs cover octal/hex aliases of 127.0.0.1. They never mention 0.0.0.1.
Webhook is the body sink. WebHookService POSTs and stores 8 KB. Maintainer has admin_web_hook.
I stood up stock gitlab/gitlab-ce:19.4.1-ce.0. Local-requests off. Catcher inside the GitLab netns on 0.0.0.1:18080. Hook create http://127.0.0.1:18080/ and http://169.254.169.254/: 422 Invalid url given. Hook create http://0.0.0.1:18080/: 201. Test push events: 201. Hook events response_body contains GITLAB-WEBHOOK-0NET-SSRF-WITNESS.
Wrong turns already recorded: /users/sign_in with Accept: application/json is 404; seed password containing root is WeakPasswords; adding 0.0.0.1 to lo puts it on Socket.ip_address_list so validate_localhost 422s the "positive"; IP_FREEBIND plus ip route add local 0.0.0.1/32 dev lo binds without joining the address list; compose $ in the password needs $$. A reverse shell. Theatre. The oracle is 201 on 0.0.0.1, 422 on 127.0.0.1, witness in the log.
exact 0.0.0.0 is not a netmask
def validate_localhost(addrs_info)
local_ips = ["::", "0.0.0.0"]
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
return if (local_ips & addrs_info.map(&:ip_address)).empty?
raise BlockedUrlError, "Requests to localhost are not allowed"
end
def validate_loopback(addrs_info)
return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
raise BlockedUrlError, "Requests to loopback addresses are not allowed"
endDeny 0.0.0.0/8 the same way 127.0.0.0/8 is denied.
I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.