Skip to content

XSS in k8s proxy abusing X-Accel-Redirect header in proxy response

⚠️ Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

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
  1. 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
  2. Now log in as a regular user
  3. Create a new group mynewgroup
  4. Create a project in the group project1
  5. In the project go to the Model registry https://gitlab.example.com/mynewgroup/config/-/ml/models
  6. Create a new model, and in the model create a new version
  7. Go to the artifacts tab https://gitlab.example.com/mynewgroup/config/-/ml/models/1/versions/1#/artifacts
  8. Upload this attack_to_upload.html 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
  9. 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  
  1. Go to https://gitlab.example.com/mynewgroup/config/-/clusters and click connect a cluster
  2. In the popup under the heading Register agent with the UI click the dropdown and select the test-agent, click Register
  3. Copy the Agent access token
  4. 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
  5. Download the file server_poc.py into the same directory, in the file update the X-Accel-Redirect header to point to your model upload from step 8
  6. 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 replace 127.0.0.1:9999 with host.docker.internal:9999, and replace gitlab.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  
  1. You should now in the browser see the test-agent going to connected state (if not refresh the browers)
  2. Now go to the project https://gitlab.com/mynewgroup/attack/-/environments and click New Environment
  3. 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 the description (again replace gitlab.example.com with your server host)
<h1><a href="https://gitlab.example.com/-/kubernetes-agent/k8s-proxy/api/v1/pods">click me</a></h1>  
  1. Click Create and take a note of the ID
  2. If you now visit https://gitlab.com/mynewgroup/attack/-/environments/ID_FROM_STEP_17 and click the Click here link, you should see an error
  3. Click "back" in the browser
  4. 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: