Stored XSS in blob viewer
: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 #3247096](https://hackerone.com/reports/3247096)** by `joaxcar` on 2025-07-10, assigned to @katwu: [Report](#report) | [Attachments](#attachments) | [How To Reproduce](#how-to-reproduce) ## Report ##### Summary Hi team, another slightly strange one, but I felt like it was worth reporting. There was a fix for the `code navigation` feature used in `blob viewers` in the latest patch, which made me have a look at it. It turns out that there is another XSS vulnerability there, but it has some constraints at the moment. This XSS worked out of the box in at least Gitlab version 17.5.1, but after that, there was a change in how `lsif` artifacts are parsed. I will go into details, but the constraint is that I dont have a way to create the artifacts needed on gitlab.com today. This is, however, not an issue if a bad actor has created an `lsif` artifact when a GitLab server was at 17.5.1. The artifact will still exist, and so will the XSS. I have tested this by setting up a GitLab 17.5.1, creating the malicious `lsif` artifact, and then upgrading the instance to the latest 18.1.2 just to check that the artifacts stick around. On Gitlab.com, it's possible to test it by just mocking the `lsif` data. ##### Technical details The blob viewer will trigger `code navigation` if the file that is rendered is `simple` (this only works for filetypes that are server-side highlighted, as `python` is for example) > app/assets/javascripts/repository/components/blob_content_viewer.vue ```js if (type === SIMPLE_BLOB_VIEWER) { eventHub.$emit('showBlobInteractionZones', this.blobInfo.path); } ``` The `code navigation` will fetch an `lsif` file with some `json` that instructs the frontend how to modify the rendered code. If you look in the `index.js`, there is a block that will run only if `end_line` does not exist in the downloaded data. This is the case for `lsif` artifacts parsed prior to GitLab 16. Somewhere around that time, a new parser was added that would add `end_lines` to the parsed data. The bad path is inside the `deprecatedNodeUpdate` > app/assets/javascripts/code_navigation/utils/index.js ```js if (d.end_line === undefined) { // For old cached data we should use the old way of parsing deprecatedNodeUpdate({ d, line, wrapTextNodes }); } else { ``` In `deprecatedNodeUpdate`, there is this line ```js const deprecatedNodeUpdate = ({ d, line, wrapTextNodes }) => { ... elm.replaceWith(...wrapNodes(elm.textContent, elm.classList, elm.dataset)); ... } ``` This line will try to wrap some text with a few `spans`. ```js const wrapNodes = (text, classList, dataset) => { const wrapper = createSpan(); // eslint-disable-next-line no-unsanitized/property wrapper.innerHTML = wrapSpacesWithSpans(text); wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text, classList, dataset)); return wrapper.childNodes; }; ``` The issue is that `elm.textContent` is passed into `wrapper.innerHTML = wrapSpacesWithSpans(text)` which essentially takes the `textContent` from a DOM node and passes it to `innerHTML`. The problem with this is that a node like this `<span>&lt;img/src/onerror=alert()&gt;</span>` will have `textContent == "<img/src/onerror=alert()>"` and thus convert to real HTML when put into `innerHTML` So a python file with this content ```python "<img/src/onerror=alert(document.domain)>" ``` and a `lsif` artifact like this ```json [{"start_line":0,"start_char":1,"definition_path":"hej.py#L5","hover":[{"value":""}]}] ``` will trigger an XSS on a self hosted server. There are multiple CSP bypasses on gitlab.com that can be used here as well but I will get back with an example of that in a comment. ##### POC This is a quick example using my gitlab instance. Use Chrome for this test 1. Visit https://gl.j15.se/test/asdf/-/blob/main/hej.py?ref_type=heads and see that there is a python file with the string `"<img/src/onerror=alert(document.domain)>"`. This is running on version 18.1.2 and has the wrong format of the `lsif` artifact. Just to show how it looks 2. Now open `devtools` and go to the network tab 3. Refresh the page 4. You will see a call to `https://gl.j15.se/test/asdf/-/jobs/100/artifacts/raw/lsif/hej.py.json?file_type=lsif` right click this call and pick `Overwrite Content` 5. There will be a small popup asking you where to store the Chrome override files, pick any directory 6. Now in the small editor that opens delete all text that is there and replace it with ```json [{"start_line":0,"start_char":1,"definition_path":"hej.py#L5","hover":[{"value":""}]}] ``` 7. Click `ctrl-s` to save 8. Refresh the page again (with devtools still open) 9. The string in the python file should now disapear and trigger an XSS alert ##### Disclaimer The json in step 6 is possible to generate on an older version of gitlab, the whole thing with devtols is NOT part of a real attack. I think that this is worth fixing for two reasons 1. There might be people who already know about this and have a payload stored even on gitlab.com. This payload will still work today 2. There might be ways to spoof the `lsif` artifact that I don't know about, or ways to generate bad artifacts today. I have just not found one yet Also, I will get back as soon as possible with an example project on gitlab.com with a full CSP bypass! ##### Examples ![Screen_Recording_2025-07-11_at_01.21.16.mov](https://h1.sec.gitlab.net/a/66d8bc89-ef60-4185-8402-833415c6f999/Screen_Recording_2025-07-11_at_01.21.16.mov) ##### What is the current *bug* behavior? `textContent` is passed into `innerHTML` which is an XSS vector ##### What is the expected *correct* behavior? Either use `innerHTML` and send it into `innerHTML` or sanatize the `textContent` Best regards Johan #### Impact Stored XSS with CSP bypass ## Attachments **Warning:** Attachments received through HackerOne, please exercise caution! * [Screen_Recording_2025-07-11_at_01.21.16.mov](https://h1.sec.gitlab.net/a/66d8bc89-ef60-4185-8402-833415c6f999/Screen_Recording_2025-07-11_at_01.21.16.mov) ## 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