XSS in k8s proxy abusing X-Accel-Redirect header in proxy response
HackerOne report #3120062 by joaxcar
on 2025-04-30, assigned to @katwu:
Report | Attachments | How To Reproduce
Report
Summary
This can be seen as a bypass to https://hackerone.com/reports/2932309 or at least building on it. The impact is the same but the path there is a bit different. (CVE-2025-0475)
When fetching data through the k8s proxy endpoint, the served content can only have one of four safe content-type
headers. And the fix for #2932309f added the X-Content-Type-Options: nosniff
header, which makes it impossible for the browser to "sniff" the content.
I found a way to bypass this using a header called X-Accel-Redirect
(see documentation), which allows us to force the Gitlab Nginx server to serve content from a different endpoint on the same host.
What can be served depends on the Nginx config. It's possible that companies could have used nginx['custom_gitlab_server_config'] to add configuration that allows this header to read local files and similar. This is not the case by default. However, what is possible is to fetch other data on the same host.
If the attacker uploads a .html
file as an ML model artifact
its possible to point the header to it like this
X-Accel-Redirect: /GROUPNAME/PROJECTNAME/-/package_files/ID/download
the response from the k8s proxy will contain the response from this endpoint. These files are served with a content-type: text/html
. The response will be a mix of the original response headers and the redirected response headers. If there is no content-type
from the k8s proxy response, Nginx will use the content-type
from the redirected response.
The package_files
endpoint returns data with content-disposition: attachment
, which protects these files from causing XSS on their own. But the "mixed response" from the k8s proxy can cause this protection to fail by adding an additional header with content-disposition: inline
. Having both these headers will cause the content to render inline.
The last issue is that package_files
are served with a cache-control: no-cache
, which means that the response is not stored in the local cache (that I used in #2932309f) but will still get stored in Chrome's "backward/forward" cache. This makes it possible to abuse either by a victim navigating using the browser's "back" or "forward" functionality, or through a malicious link that uses JavaScript to navigate a page.
Again, this does not impact .com
as the KAS domain is under another subdomain. But has a high
impact on self-hosted servers where KAS is on by default and is served under the same domain.
Note on CSP
If there is a CSP on the instance, this will actually now be sent in the k8s-proxy
response as the response is now coming from /GROUPNAME/PROJECTNAME/-/package_files/ID/download
. This can be bypassed using a CSP bypass I have outlined in previous reports using iframes
(as we fully control the HTML being served)
I will submit a CSP bypass POC in a comment to the initial report to avoid cluttering the main issue.
The CSP bypass means that we have XSS even on self-hosted servers that enable strict CSP.
Steps to reproduce
- Set up a self-hosted Gitlab instance, I don't know if you need SSL configured but its probably good to set it up (my test setup has it
- Now log in as a regular user
- Create a new group
mynewgroup
- Create a project in the group
project1
- In the project go to the Model registry
https://gitlab.example.com/mynewgroup/config/-/ml/models
- Create a new model, and in the model create a new
version
- Go to the artifacts tab
https://gitlab.example.com/mynewgroup/config/-/ml/models/1/versions/1#/artifacts
- Upload this
file, and take a note of the "download URL" for the file, it should be like
https://gitlab.example.com/mynewgroup/config/-/package_files/1/download
- In the project
mynewgroup/project1
create a file with the name.gitlab/agents/test-agent/config.yaml
and the content (replace groupname if you have another)
user_access:
access_as:
agent: {}
groups:
- id: mynewgroup
- Go to https://gitlab.example.com/mynewgroup/config/-/clusters and click
connect a cluster
- In the popup under the heading
Register agent
with the UI click the dropdown and select thetest-agent
, click Register - Copy the
Agent access token
- In a terminal on your local computer, create a file called
glab-agentk-token-local
and paste the token from step 10 in the file - Download the file
into the same directory, in the file update the
X-Accel-Redirect
header to point to your model upload from step 8 - Open another terminal tab in the same directory, in one of the tabs run
python3 server.py
and in the other tab run this command (this command is made to work on linux, if you use MacOS replace127.0.0.1:9999
withhost.docker.internal:9999
, and replacegitlab.example.com
with your server domain)
docker run \
--network host \
--rm \
-it \
-v ./glab-agentk-token-local:/etc/agentk/secrets/token \
-e POD_NAMESPACE=agentk-nsname \
-e POD_NAME=agent-podname registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:latest \
--kas-address=wss://gitlab.example.com/-/kubernetes-agent/ \
--token-file=/etc/agentk/secrets/token \
-s 127.0.0.1:9999
- You should now in the browser see the test-agent going to connected state (if not refresh the browers)
- Now go to the project https://gitlab.com/mynewgroup/attack/-/environments and click New Environment
- Give the environment a name, then add
https://example.com
as the External URL, and select test-agent as the Gitlab Agent. And put this as thedescription
(again replacegitlab.example.com
with your server host)
<h1><a href="https://gitlab.example.com/-/kubernetes-agent/k8s-proxy/api/v1/pods">click me</a></h1>
- Click Create and take a note of the ID
- If you now visit
https://gitlab.com/mynewgroup/attack/-/environments/ID_FROM_STEP_17
and click theClick here
link, you should see an error - Click "back" in the browser
- Click "forward" in the browser. The XSS should pop
It's possible to remove the last two manual navigations by adding an "attacker page", but as the policy is written to lower the impact of such attacks, I think this proves that it can happen without one.
Screen_Recording_2025-04-29_at_15.17.53.mov
Impact
stored XSS on self-hosted GitLab where the KAS server is running in the default config
What is the current bug behavior?
The k8s-proxy
endpoint does not filter X-Accel-Redirect
headers which can cause Nginx to serve unintended content.
What is the expected correct behavior?
X-Accel-Redirect
should be filtered before being passed along to Nginx
Impact
stored XSS on self-hosted GitLab where the KAS server is running in the default config
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section: