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
•Technically reviewed by Anupam Mediratta•Published September 17, 2026•Reviewed 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

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.

critical

navbar.html Injection via innerHTML in loadNavbar.js

An Electron app's `loadNavbar.js` fetched `navbar.html` and wrote the response straight into `innerHTML`, so any script tags or event-handler attributes in that file would execute in the renderer. The fix adds a `sanitizeNavbarHtml` routine that parses the markup with `DOMParser` and strips `<script>` tags, `on*` attributes, and `srcdoc` before the content is injected into the DOM.

critical

addSourceInput() javascript: URL Injection in Source List Handler

The `addSourceInput()` function in the application's source list handler was assigning user-provided URLs directly to input elements without protocol validation, allowing `javascript:` URLs to persist and execute. A new `isSafeSourceUrl()` helper now restricts values to HTTP/HTTPS protocols or empty strings.

critical

getCheckboxString() HTML Injection via MIDI Metadata

The `getCheckboxString` helper built checkbox markup by interpolating MIDI instrument and program names directly into an HTML template string, which was then parsed with `DOMParser` and injected via `replaceChildren()`. A crafted MIDI file could smuggle an HTML/JS payload through its instrument metadata and have it rendered as live DOM, including inline event handlers. The fix adds a dedicated `escapeHtml()` function and routes both parameters through it before the markup is built.