Affected Versions
| Affected | DataTables RowGroup extension, versions prior to fix commit |
| Fixed in | unknown (first-party code fix) |
| Ecosystem | npm |
| CVE / GHSA | not assigned |
| CWE | CWE-79 (Improper Neutralization of Input During Web Page Generation) |
The Vulnerability Explained
The DataTables RowGroup extension provides collapsible grouping rows based on column data. When a table initializes with rowGroup: { dataSrc: ... }, the extension calls startRender to generate the content for each grouping header.
The default startRender implementation was:
startRender(rows, group, level) {
return group;
}
This return value flowed directly into cell.html(display) — a jQuery method that parses strings as HTML. When group contained data from the dataSrc column, and that data included malicious markup, the browser executed it.
Consider a DataTable configured with:
$('#example').DataTable({
data: [['Name', 'Role', '<img src=x onerror=alert(1)>', 61, 'Date', '$0']],
rowGroup: {
dataSrc: 2 // Points to the malicious payload column
}
});
The string <img src=x onerror=alert(1)> becomes the group parameter. Returned directly, it reaches cell.html() and creates an image element whose onerror handler fires arbitrary JavaScript. This executes in the user's session, with access to cookies, localStorage, and the ability to perform actions as that user.
The vulnerability is stored in nature when the malicious data persists in a database, and reflected when loaded from transient sources. Both vectors exploit the same trust boundary violation: RowGroup treated data as markup rather than text.
The Fix
The correction modifies the default startRender to escape HTML entities before returning:
startRender(rows, group, level) {
return util.escapeHtml(group);
}
The util.escapeHtml() function converts characters with special meaning in HTML (<, >, ", ', &) into their corresponding entities. The payload <img src=x onerror=alert(1)> becomes <img src=x onerror=alert(1)>, which renders visually but never executes.
This change preserves the intended display of group labels while removing the injection vector. The fix is minimal and surgical — only the default callback changes. Applications with custom startRender implementations must audit their own handling of group data.
The test suite validates the fix against concrete attack payloads:
it('Escapes an <img onerror> payload found in the grouping data cell', function() {
// ... table initialization with malicious dataSrc ...
expect(groupRow.text()).toBe('<img src=x onerror=alert(1)>');
expect(groupRow.find('img').length).toBe(0);
});
The assertion confirms that the escaped string appears as text content, and no DOM element is created from the payload.
Key Takeaways
-
Default callbacks must be secure by default: A generic
return groupin a rendering callback assumes data is safe, which fails when data originates from user input. Secure defaults can be escaped; intentional HTML requires explicit opt-in. -
html()is a sink: Any method namedhtmlor similar (innerHTML, outerHTML, insertAdjacentHTML) that parses strings as markup requires scrutiny. Pass only literals or escaped values. -
DataTables
dataSrcis a trust boundary: The property specifies where grouping data originates, and that origin may be attacker-controlled. Treat alldataSrcvalues as untrusted in rendering paths. -
util.escapeHtml()is the correct primitive: When text must display without interpretation, entity encoding is the robust solution. This differs from JavaScript escaping, URL encoding, or CSS escaping — each has its appropriate context. -
Row grouping headers have elevated privilege: Group rows appear at the top of table sections, often with styling that draws user attention. XSS here is highly visible and likely to be clicked or interacted with.
How Orbis AppSec Detected This
Source: The group parameter in the startRender callback, populated from DataTable.util.get(fns[level]) reading the dataSrc column data
Sink: cell.html(display) invocation that parses the return value as HTML
Missing control: No HTML entity encoding or sanitization between source and sink; the callback returned raw data directly
CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Fix: Wrap the group return value with util.escapeHtml() to force textual interpretation
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability in DataTables RowGroup demonstrates how even mature UI libraries can harbor XSS in their default configurations. The startRender callback's direct return of grouping data to an HTML parser created a reliable injection point for any attacker with control over table contents. The util.escapeHtml() fix restores the intended text-only semantics while maintaining backward compatibility for intentional overrides. Developers using DataTables should verify their rowGroup configurations and audit any custom render callbacks for similar patterns.