Back to Blog
critical SEVERITY4 min read

DataTables RowGroup startRender XSS via Unescaped Group Data

DataTables RowGroup's default `startRender` callback inserted group labels directly into the DOM using HTML-aware methods, enabling XSS when user data reached the `dataSrc` property. The fix applies `util.escapeHtml()` to neutralize malicious payloads before insertion.

O
By Orbis AppSec
Published September 17, 2026Reviewed September 17, 2026

Answer Summary

The DataTables RowGroup extension's default `startRender` callback in affected versions rendered group data via `cell.html(display)`, which parses its argument as HTML. An attacker controlling data in the column specified by `rowGroup.dataSrc` could inject `<img onerror>` or `<script>` payloads that execute in the victim's browser session. The fix replaces direct return of `group` with `util.escapeHtml(group)`, forcing textual rendering. No CVE has been assigned. CWE-79 (Improper Neutralization of Input During Web Page Generation) applies.

Vulnerability at a Glance

cweCWE-79
fixutil.escapeHtml() wrapper prevents HTML interpretation
riskStored/Reflected XSS in data grouping headers
languageTypeScript/JavaScript
root causestartRender callback returned unsanitized data to HTML insertion method
vulnerabilityCross-site Scripting (XSS)

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 &lt;img src=x onerror=alert(1)&gt;, 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 group in 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 named html or similar (innerHTML, outerHTML, insertAdjacentHTML) that parses strings as markup requires scrutiny. Pass only literals or escaped values.

  • DataTables dataSrc is a trust boundary: The property specifies where grouping data originates, and that origin may be attacker-controlled. Treat all dataSrc values 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.

Prevention and further reading

Frequently Asked Questions

If I override startRender with my own function, does this vulnerability still affect my DataTable configuration?

Only if your custom callback also returns unsanitized data. The default implementation was vulnerable; custom callbacks using `cell.html()` or similar methods with user-controlled data remain at risk unless they apply `util.escapeHtml()` or equivalent.

Does the dataSrc property accept only integer column indices, or can named properties also trigger this XSS?

Both integer indices and property names resolve data that reaches `startRender`. Any data path—`dataSrc: 2`, `dataSrc: 'department'`, or nested accessors—that returns attacker-controlled strings creates the same HTML injection surface.

Is the fix backward-compatible for applications that intentionally render HTML in group headers?

Yes, but with changed behavior. Applications relying on intentional HTML in group data must now override `startRender` and explicitly skip escaping. The secure-by-default posture protects unaware users while preserving extension points for intentional HTML use.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #23

Related Articles

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

shell-quote 1.8.3: Line Terminator Command Injection (CVE-2026-9277)

CVE-2026-9277 is a critical command injection vulnerability in shell-quote versions before 1.9.0, where unescaped line terminators allow attackers to break out of quoted strings and execute arbitrary shell commands. The fix upgrades the dependency across multiple React Native CLI packages and related libraries through npm overrides.