GitLab, Gitea HTTPS import drops the UrlBlocker pin
GitLab 19.4.1 Gitea HTTPS import throws away the IP-pinned URI and reconnects to the original hostname. Rebind that name to loopback and Faraday GETs it. Repo-list JSON comes back on status.json. HTTP imports keep the pin. No CVE yet.
- Name
- GitLab, Gitea HTTPS import drops the UrlBlocker pin
- 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. Signed-in user, Gitea import, personal namespace. Lab image gitlab/gitlab-ce:19.4.1-ce.0.
- Published
- 01 Oct 2026
- Updated
- 01 Oct 2026
- Tags
- 0-day, gitlab, ssrf, dns-rebind, cwe-918, cwe-367, authenticated
https throws the pin away
The proof of concept is on GitHub: abraxas/gitlab-gitea-import-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 / CWE-367. 7.7 High (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N). Authenticated Gitea import. Not unauthenticated. Not host RCE.
Same product, siblings: Gitaly fetch omits resolved_address, webhook 0.0.0.0/8.
What an attacker can do
Set Gitea Host URL to https://<name-they-control>/. UrlBlocker allows the first public A-record. HTTPS client_options keeps that hostname. Rebind to 127.0.0.1. Faraday GETs the catcher on the GitLab worker loopback. GET /import/gitea/status.json provider_repos is whatever the catcher returned.
On all-in-one omnibus that is Workhorse, Puma, exporters. HTTP Host URL stays on the pinned IP. Direct https://127.0.0.1 is blocked.
How I found it
I read the 23 September 2026 19.4.1 / 19.3.3 / 19.2.7 patch notes first, then the v19.4.1-ee tree. CI regex RCE, MR diff XSS, Workhorse file.path are closed on this pin. Import SSRF was not finished.
Import::GiteaController#client_options is the leftover. HTTP uses the IP-pinned verified_url. HTTPS uses provider_url, the name the user typed. LegacyGithubImport::Client builds Octokit api_endpoint from that host. Gitlab::Octokit::UrlValidation calls validate! then @app.call(env) with no rewrite.
I stood up stock gitlab/gitlab-ce:19.4.1-ce.0. Local-requests off. Lab CA in /etc/gitlab/trusted-certs because Faraday ssl.verify is true. DNS for gitea.lab: lookups 1-2 return 1.1.1.1, later lookups return 127.0.0.1. Catcher shares the GitLab netns on :443. status.json returned provider_repos named GITLAB-GITEA-IMPORT-SSRF-WITNESS. Catcher log: GET /api/v1/user/repos peer=127.0.0.1 host=gitea.lab.
Direct https://127.0.0.1/: "Specified URL cannot be used". HTTP http://gitea.lab/ stayed on the pin and did not return the witness.
Wrong turns already recorded: /users/sign_in with Accept: application/json is 404 (HTML is 200); seed password CveLabRoot9! rejected because it contains root; CA bind-mount ro is EROFS after reconfigure links the file; first-two-then-loopback on lookup 2 makes UrlValidation block before Faraday connects. Pairs mode (odd public, even loopback) is what landed. A reverse shell. Theatre. The oracle is the witness in status.json plus the catcher peer.
https keeps the name
def client_options
verified_url, provider_hostname = verify_blocked_uri
{
host: verified_url.scheme == 'https' ? provider_url : verified_url.to_s,
api_version: 'v1',
hostname: provider_hostname
}
endUse verified_url.to_s for https the same way HTTP already does. Rewrite env[:url] in Octokit UrlValidation to the pinned IP.
I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.