SSRF via Github importer while importing GitHub repository with markdown image link contains malicious URL
HackerOne report #2249268 by imrerad
on 2023-11-12, assigned to @greg:
Report | Attachments | How To Reproduce
Report
Summary
Gitlab features multiple importers that you can use to migrate your repositories from external sources (e.g. Github.com, Bitbucket Cloud, etc.). The Github importer also supports fetching "issue links", which is usually media content referenced in the original source's issues. The implementation aims to fetch the media content from the trusted source (Github.com) only, but this security measure does that by checking whether the reference starts with the whitelisted URL prefix. The relevant parts of the source code:
https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/github_import/markdown_text.rb#L13
https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/github_import/markdown/attachment.rb#L98
https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/github_import/markdown/attachment.rb#L28
https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/github_import/markdown/attachment.rb#L58
To summarize, they verify whether the reference begins with the following URL:
GITHUB_MEDIA_CDN = 'https://user-images.githubusercontent.com'
An attacker could create a DNS record for the hostname user-images.githubusercontent.com.attacker.controlled.domain
and the Gitlab server would fetch contents from here. HTTP redirects are followed by this HTTP client, turning this into an SSRF GET primitive. The attacker has no control over the headers but the response body is reflected (it is uploaded on the Gitlab server and can be retrieved via an uploads URL).
The Gitlab server features efficient SSRF protection measures:
https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/url_blocker.rb
There are various options and can be fine tuned in the server settings. This attack relies on the "allow_local_requests_from_web_hooks_and_services". This setting is not documented to be security sensitive. Furthermore, according to an old ticket, it is likely widely used by corporate customers:
"Thankfully, allow_local_requests_from_web_hooks_and_services is disabled by default, but as Dominic says, corporates may use it a lot."
In the Security section of the Gitlab import feature documentation it is highlighted to import repositories from trusted source only. In this attack, the source Github.com repo is owned by the victim and as such it is trusted. The attacker only needs permissions to open Issues in there - which is the default setup for public repositories.
Steps to reproduce
Preparation:
- Install Gitlab server
- In the settings enable
allow_local_requests_from_web_hooks_and_services
: http://172.21.12.214/admin/application_settings/network, expand the Outbound requests section - In the settings, enable Github import: http://172.21.12.214/admin/application_settings/general, expand the import/export section.
As the attacker:
- setup a DNS record with the host pattern shown above. During my research, I was using https://www.cloudns.net/ and created
user-images.githubusercontent.com.geza.cloudns.cl
- obtain TLS certificates for this hostname (e.g. by using Let's encrypt)
- start the glab_attachments python script:
root@test-instance-1:/etc/letsencrypt/live/user-images.githubusercontent.com.geza.cloudns.cl# python3 glab_attachments.py --data-to-serve /home/radimre83/1x1.png --certificate ./fullchain.pem --private-key ./privkey.pem --redirect-to http://127.0.0.1:8060/rails-metrics
This script returns the specified file for Github.com, and returns a redirect response when it is invoked by Gitlab.
- open an Issue in the victim repository and use the following Markdown description:

The attached png file is a 1x1 transparent image to demonstrate the stealthy nature of this attack - you won't see it in your browser (neither at Github, nor at Gitlab).
As the victim:
- import this repository with attachments_import setting turned on. If you use the webui, you can initiate a new import at creating a new repository, select import, push the Github.com button and there pay attention to tick the "Import Markdown attachments (links) " checkbox.
Now inspect the destination repository. The picture won't show up, inspect the description of the issue, you will find the reference like this:
http://gitlab.example.com/root/Issue-attachment12/uploads/3eb6ba7aa4a167b54cd6eed463ef93c2/1.png
When opening this link, you will find the snapshot of http://127.0.0.1:8060/rails-metrics saved during the import process:
### HELP action_cable_active_connections Multiprocess metric
### TYPE action_cable_active_connections gauge
action_cable_active_connections{pid="puma_0"} 0
action_cable_active_connections{pid="puma_1"} 0
action_cable_active_connections{pid="puma_2"} 0
action_cable_active_connections{pid="puma_3"} 0
...
This resource should be accessible on the Gitlab server only (from 127.0.0.1) - see /var/opt/gitlab/nginx/conf/nginx-status.conf
the corresponding nginx entry.
Impact
The attacker could send GET requests to arbitrary private network endpoints of the Gitlab server. This includes:
- services listening on the loopback interface
- services listening on the link-local interface
- services listening on a private LAN (10.0.0.0/8, 192.168.0.0/16, etc)
The attacker could access this without even having an account on the target Gitlab server.
Some network endpoints disclose sensitive data without authentication, e.g. AWS EC2 IMDSv1 or Kubernetes's kubelet readonly port (10255); the impact of unauthorized access to these two is likely critical.
What is the current bug behavior?
The importer of the Gitlab server fetches content from external, attacker controlled sources due to not restricting the source hostname properly:
GET /hack1980vs2024.png HTTP/1.1
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: */*
User-Agent: Ruby
Connection: close
Host: user-images.githubusercontent.com.geza.cloudns.cl
What is the expected correct behavior?
The Gitlab server should either throw an error or ignore these malicious references as is. This could be done by comparing the prefix of the reference URL along with the first / character after the hostname.
Relevant logs and/or screenshots
Two screenshots attached.
Results of GitLab environment info
### gitlab-rake gitlab:env:info
System information
System:
Proxy: no
Current User: git
Using RVM: no
Ruby Version: 3.0.6p216
Gem Version: 3.4.19
Bundler Version:2.4.20
Rake Version: 13.0.6
Redis Version: 7.0.13
Sidekiq Version:6.5.7
Go Version: unknown
GitLab information
Version: 16.5.1-ee
Revision: 55da9ccb652
Directory: /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 13.11
URL: http://glab.local
HTTP Clone URL: http://glab.local/some-group/some-project.git
SSH Clone URL: git@glab.local:some-group/some-project.git
Elasticsearch: no
Geo: no
Using LDAP: no
Using Omniauth: yes
Omniauth Providers:
GitLab Shell
Version: 14.29.0
Repository storages:
- default: unix:/var/opt/gitlab/gitaly/gitaly.socket
GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
Gitaly
- default Address: unix:/var/opt/gitlab/gitaly/gitaly.socket
- default Version: 16.5.1
- default Git Version: 2.42.0
Impact
See inline above.
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section: