highlightjs / highlightjs/highlight.js

Security: ReDoS in 15 grammars (17 regexes) - 6 live-measured quadratic cases (v11.12.0)

Open
#4,529 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
25k
Forks
3.8k
Avg merge
11h 35m
Merged PRs (30d)
3

Description

## Security: ReDoS in 15 grammars (17 regexes) — 6 live-measured quadratic cases (v11.12.0)

### Summary

A re-audit of the grammars shipped with 11.12.0 (recheck 4.5.0 automaton analysis plus
live measurements) found **17 ReDoS-susceptible regexes across 15 grammar files**.
`hljs.highlight()` is synchronous and single-threaded; crafted inputs of tens of KB
stall the event loop for seconds, superlinear in input size.

This is the same class fixed in 10.4.1 (GHSA-7wwv-vh3v-89cq) and again in 11.12.0
(#4362, #4361). Version archaeology across 16 npm releases (9.18.5 → 11.12.0) shows
several flagged patterns shipped **inside** the 10.4.1 fix release, and grammar rewrites
since then have repeatedly reintroduced the class — four of them in 11.12.0 alone
(dart ×2, fsharp, json). The per-pattern table below (hopefully) lets you fix the family
in one pass rather than issue-by-issue.

### Live-confirmed quadratic cases (×4 per input doubling; Node v24.14.0, 2026-09-01)

| grammar (file:line, `es/` build, 11.12.0) | pattern | payload → time @ 40 KB |
|---|---|---|
| `livescript.js:285` | `/(?![ *])(\\.|[^\\\n])*?/[gim]*(?=\W)` | `"/" + "a"*n` → **11.0 s** |
| `stan.js:485` | `\s*\w+(?=\s*[\(.*\)])` | `"a"*n` → 4.2 s |
| `csharp.js:181` | `"""("*)(?!")(.|\n)*?"""\1` | crafted → 3.2 s |
| `perl.js:266` | `\s+:\s*\w+(\s*\(.*?\))?` | `":" + "a"*n` → 1.37 s |
| `scilab.js:59` | `[a-zA-Z_][a-zA-Z_0-9]*[\.']+` | `"a"*n` → 1.26 s |
| `bash.js:114` | `\w[\w\d_]*\s*\(\s*\)\s*\{` | `"a"*n` → 1.13 s |

Extrapolation: livescript 11 s @ 40 KB ⇒ ~2.1 h @ 1 MB, single call, single event loop.

Minimal PoC (largest measured case):

```js
// npm i highlight.js@11.12.0
const hljs = require("highlight.js");
for (const n of [10_000, 20_000, 40_000]) {
const t = process.hrtime.bigint();
hljs.highlight("/" + "a".repeat(n), { language: "livescript" });
console.log(n, Number(process.hrtime.bigint() - t) / 1e6, "ms");
}
// 10000 -> ~585 ms, 20000 -> ~2336 ms, 40000 -> ~10981 ms (×4 per doubling)
```

csharp payload: `"""` + `\\`.repeat(30) + `"` + `"a".repeat(n)` + `"` + `\\`.repeat(29).

### recheck-flagged, worst case not reproduced by my manual payloads (for maintainer verification)

`json.js:20` (first affected 11.12.0), `fsharp.js:74` (11.12.0), `dart.js:230+231`
(11.12.0), `markdown.js:25+26` (≤10.4.1), `r.js:108` (≤10.4.1), `rust.js:212` (11.0.x),
`powershell.js:242` (≥9.18.5), `xquery.js:316` (10.5.0).

Transparency: `zephir.js:76` was flagged by recheck but measured **linear** live
(8 ms @ 1.25 K → 193 ms @ 40 K) — included so you can re-verify and drop it if your
tooling agrees.

### Version archaeology (16 npm releases tested, tarball-level)

| Cohort | Patterns | Meaning |
|---|---|---|
| Present in 9.18.5 **and** 10.4.1 (the "fixed" release) through 11.12.0 | bash:114, livescript:285, powershell:242, zephir:76 | shipped inside the GHSA-7wwv fix release; still present today |
| First present at 10.4.1 | markdown:25/26, r:108, scilab:59 | likewise present in the fix release |
| Introduced after 10.4.1 | xquery:316 (10.5.0), rust:212 (11.0.x), stan:485 (11.4.0), csharp:181 + perl:266 (11.10.0), dart:230/231 + fsharp:74 + json:20 (11.12.0) | grammar evolution reintroduced the class |

8 of the 15 grammars (csharp, livescript, markdown, perl, powershell, r, scilab,
xquery) were named in GHSA-7wwv-vh3v-89cq; bash, dart, fsharp, json, rust, stan,
zephir were not covered by that advisory.

### Suggested fix (consistent with your precedents #4413/#4462 and the 10.4.1 mass-fix)

1. Measured quadratic set: bound or disambiguate the quantified run —
- `bash:114`: `\w[\w\d_]*` is semantically `\w+`; bound it (e.g. `{0,256}`) per the
#4413/#4462 style.
- `scilab:59`, `stan:485`, `perl:266`: same treatment for the unbounded runs that
precede a rarely-present tail.
- `livescript:285`: bound the regex-literal body or restructure as
`(?:[^\\\n\\]|\\[\s\S]){0,4096}?`.
- `csharp:181`: replace `(.|\n)*?` with quote-aware runs `(?:[^"]|"(?!""))*?`
(keep the backreference; verify with the verbatim-string tests).
2. recheck-flagged set: run candidate rewrites through recheck. The four 11.12.0
additions are recent grammar contributions — a recheck CI gate on
`src/languages/*.js` (automating the 2020 manual audit in #2855) would stop this
cohort from recurring.

### Workarounds for downstream users

Cap untrusted input length (~10–20 KB) before `highlight()`; highlight in a worker with
a timeout; avoid registering affected grammars (aliases share the risk).

---

Credits: **LeoWSY-hashblue**. I have a MITRE CVE request pending for this family and
will reference this issue; happy to coordinate on wording/timing.

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the Node.js PoC through hljs.highlight(), then inspect the listed patterns in src/languages/*.js and compare the prior fixes in #4413 and #4462. Run the recheck analysis and the relevant grammar tests, including the verbatim-string tests for csharp. Done means the measured quadratic cases are resolved, the flagged additions are verified, and the grammar set has regression coverage or a recheck CI gate.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.