Environment
ESLint version: v10.8.1
@eslint/markdown version: 8.0.3
Node version: v24.14.1
npm version: v11.11.0
Operating System: darwin 25.6.0 (macOS 26.6.2)
Which language are you using?
commonmark (also reproduces under gfm)
What did you do?
no-reference-like-urls's regex has the exact same shape of vulnerability that no-reversed-media-syntax had, fixed in #693. This rule just never got the same fix. Both rules are recommended, so any project using the recommended config has this on by default.
// eslint.config.mjs
import markdown from "@eslint/markdown";
export default [
{
files: ["**/*.md"],
plugins: { markdown },
language: "markdown/commonmark",
rules: { "markdown/no-reference-like-urls": "error" },
},
];
Generate a single-line fixture and lint it:
node -e 'const l = "[", r = "]"; require("fs").writeFileSync("repro.md", l + "()".repeat(28) + r + "(http://example.com \"a\\\"b\")\n")'
npx eslint repro.md
This is a complete, well-formed inline link (square-bracket label, then a parenthesized URL and title) — not a malformed one. The hang happens while the rule is still trying to parse the label, before it even gets to comparing the URL against known reference identifiers.
What did you expect to happen?
ESLint finishes near-instantly and reports nothing (there's no reference definition anywhere in the file for the rule to match against, so linting this file should be a no-op for this rule regardless of file size).
What actually happened?
Wall-clock time for npx eslint repro.md, varying only the repeat count n in '()'.repeat(n):
| n |
eslint runtime |
| 18 |
0.74s |
| 20 |
0.92s |
| 22 |
2.64s |
| 24 |
9.80s |
| 26 |
37.57s |
| 28 |
149.43s |
| 30 |
219.33s |
(Measured with time npx eslint repro.md. Absolute times will vary by machine, but the exponential growth pattern itself shouldn't be hardware-specific.)
Going from n=18 to n=30 (just 12 more repeats) pushes the runtime from under a second to 219s — exponential, not linear — and it pins one CPU core near 100% the whole time. --debug gives no indication of which rule is stuck.
Calling the rule's own regex directly (no ESLint involved) shows the same curve, confirming the regex itself is the bottleneck and not something in the rule's surrounding logic:
const linkOrImagePattern =
/\[(?<label>(?:\\.|[^()\\]|\([\s\S]*\))*?)\]\((?<destination>[ \t]*\r?\n?(?<![ \t])[ \t]*(?:<[^>]*>|[^ \t()]+))(?:[ \t]*\r?\n?(?<![ \t])[ \t]*(?:"[^"]*"|'[^']*'|\([^)]*\)))?[ \t]*\r?\n?(?<![ \t])[ \t]*\)$/u;
const l = "[", r = "]";
const testInput = l + "()".repeat(24) + r + "(http://example.com \"a\\\"b\")";
linkOrImagePattern.exec(testInput);
// ~9s just for this one exec() call
Link to Minimal Reproducible Example
N/A — the snippet above is a complete, self-contained reproduction.
Participation
AI acknowledgment
Additional comments
Suggested fix. Apply the same fix used in #693 to linkOrImagePattern — bound \([\s\S]*\) to \([^()]*\).
- /\[(?<label>(?:\\.|[^()\\]|\([\s\S]*\))*?)\]\(.../u;
+ /\[(?<label>(?:\\.|[^()\\]|\([^()]*\))*?)\]\(.../u;
I ran this rule's existing test suite against the fix and all 82 tests pass.
There's a side effect here that's different in kind from #693's trade-off, though. For no-reversed-media-syntax, paren structure itself is what the rule is judging, so limiting nesting depth was a meaningful semantic restriction. This rule only cares whether the URL matches a reference definition, so nested parens in the label shouldn't matter to it at all — yet with this fix, a label with 2+ levels of nesting fails to match the regex entirely, so the destination never gets extracted and the check is silently skipped even when the URL genuinely matches a definition. For example, a link whose label is see (x (a (b)) y) and whose destination is https://example.com/doc extracts destination fine under the current regex, but fails to match at all under the bounded one.
Disclosure: I'm a participant in open source contribution program OSSCA.
Environment
ESLint version: v10.8.1
@eslint/markdown version: 8.0.3
Node version: v24.14.1
npm version: v11.11.0
Operating System: darwin 25.6.0 (macOS 26.6.2)
Which language are you using?
commonmark (also reproduces under gfm)
What did you do?
no-reference-like-urls's regex has the exact same shape of vulnerability thatno-reversed-media-syntaxhad, fixed in #693. This rule just never got the same fix. Both rules arerecommended, so any project using the recommended config has this on by default.Generate a single-line fixture and lint it:
node -e 'const l = "[", r = "]"; require("fs").writeFileSync("repro.md", l + "()".repeat(28) + r + "(http://example.com \"a\\\"b\")\n")' npx eslint repro.mdThis is a complete, well-formed inline link (square-bracket label, then a parenthesized URL and title) — not a malformed one. The hang happens while the rule is still trying to parse the label, before it even gets to comparing the URL against known reference identifiers.
What did you expect to happen?
ESLint finishes near-instantly and reports nothing (there's no reference definition anywhere in the file for the rule to match against, so linting this file should be a no-op for this rule regardless of file size).
What actually happened?
Wall-clock time for
npx eslint repro.md, varying only the repeat countnin'()'.repeat(n):(Measured with
time npx eslint repro.md. Absolute times will vary by machine, but the exponential growth pattern itself shouldn't be hardware-specific.)Going from n=18 to n=30 (just 12 more repeats) pushes the runtime from under a second to 219s — exponential, not linear — and it pins one CPU core near 100% the whole time.
--debuggives no indication of which rule is stuck.Calling the rule's own regex directly (no ESLint involved) shows the same curve, confirming the regex itself is the bottleneck and not something in the rule's surrounding logic:
Link to Minimal Reproducible Example
N/A — the snippet above is a complete, self-contained reproduction.
Participation
AI acknowledgment
Additional comments
Suggested fix. Apply the same fix used in #693 to
linkOrImagePattern— bound\([\s\S]*\)to\([^()]*\).I ran this rule's existing test suite against the fix and all 82 tests pass.
There's a side effect here that's different in kind from #693's trade-off, though. For
no-reversed-media-syntax, paren structure itself is what the rule is judging, so limiting nesting depth was a meaningful semantic restriction. This rule only cares whether the URL matches a reference definition, so nested parens in the label shouldn't matter to it at all — yet with this fix, a label with 2+ levels of nesting fails to match the regex entirely, so the destination never gets extracted and the check is silently skipped even when the URL genuinely matches a definition. For example, a link whose label issee (x (a (b)) y)and whose destination ishttps://example.com/docextractsdestinationfine under the current regex, but fails to match at all under the bounded one.