Skip to content

🐛 FIX: resolve regex vulnerabilities#148

Open
KyleKing wants to merge 5 commits into
executablebooks:masterfrom
KyleKing:fix/texmath-redos-143
Open

🐛 FIX: resolve regex vulnerabilities#148
KyleKing wants to merge 5 commits into
executablebooks:masterfrom
KyleKing:fix/texmath-redos-143

Conversation

@KyleKing

Copy link
Copy Markdown
Contributor

Address confirm vulnerabilities from #143

Does not fully resolve the issue because there are unconfirmed/low risk vulnerabilities to consider and we don't know what was initially found that led to the ticket

@KyleKing KyleKing changed the title fix: address confirmed regex vulnerabilities 🐛 FIX: resolve regex vulnerabilities Jul 21, 2026
Hoist inline and string regexes to module-level compiled patterns (amsmath,
tasklists, dollarmath, anchors) so the ReDoS guard discovers them.

Compiling is also faster for these hot paths. Inline re.match(str, ...) still
compiles once and caches, but pays a cache lookup on every call, so a compiled
object runs about 2.2x faster per call (102 vs 235 ns on py3.14). The one-time
compile (5 to 59 us) is paid once either way, and module globals sidestep
re._cache's 512-entry eviction.

Benched with timeit (best-of-5, 2M calls) comparing compiled.match(text)
against a cache-warm re.match(pattern, text).

@chrisjsewell chrisjsewell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python already caches regex compilation, so it is not necessary to have any of these global variables

@KyleKing

Copy link
Copy Markdown
Contributor Author

I know, but those are for the script in tests/test_redos.py to check them and compilation is on the µs scale compared to the overall overhead of Python + mdformat's architecture

We could remove that general check file entirely if you don't want to have it fuzz test moving forward, but the alternative to use lazy compilation while also fuzzing is to walk the AST, which makes that script much more complicated

TLDR: delete tests/test_redos.py and go back to lazy compilation?

@KyleKing
KyleKing requested a review from chrisjsewell July 21, 2026 05:27

@chrisjsewell chrisjsewell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed by checking the branch out and measuring the before/after behavior directly. The core fixes are correct and the suite is green (544 passed).

ReDoS fixes — confirmed real and resolved:

  • texmath ```math block rules: the unpatched ^{3}math\s+?([^]+?)\s+?{3}grows super-linearly on `` ```math `` + a whitespace run with no closing fence — I measured **0.14 s / 1.1 s / 7.5 s** at 400 / 800 / 1600 chars. The rewritten pattern stays flat (**<3 ms** at 200k chars). Same class of fix applied toadmon`.
  • The generic tests/test_redos.py guard discovers all 27 shipped patterns (including the ones nested in texmath's rules dict) and would catch a reverted texmath fix — verified.

Precompile cleanups (amsmath RE_OPEN, anchors, dollarmath label normalizer, tasklists): behavior-preserving, verified parity on valid/invalid inputs. Good hygiene to bundle in.

Three notes (details inline):

  1. tests/test_admon.py — the new test_title_parsing_redos doesn't reproduce the blow-up; its input passes on the unpatched regex, so it doesn't guard the fix. Easy to strengthen.
  2. texmath — the new capture trims surrounding whitespace / rejects whitespace-only content. Harmless, just flagging.
  3. No CHANGELOG.md entry. Since this is the security-relevant fix (and #149 added one), it'd be good to record it here too, ideally noting which plugins/flavors were affected.

Nothing here blocks merging once the admon test is tightened. Nice work, and thanks for adding the categorical guard rather than just patching the two regexes.


Generated by Claude Code

Comment thread tests/test_admon.py Outdated
Comment thread mdit_py_plugins/texmath/index.py
Comment thread tests/test_redos.py Outdated

Copy link
Copy Markdown
Member

Thanks @KyleKing — yeah, let's go back to lazy compilation.

To be clear, it's not the perf: you're right that the compile cost is µs-scale and immaterial next to Python startup + everything downstream. My concern is structural — I don't want the library's module layout (a set of new top-level re.Pattern globals) to be driven by what the test file can introspect. That's the test tail wagging the library dog. The actual security fix here is just the two pattern rewrites — texmath's ```math rules and admon's [^"]+[^"\s](?:[^"]*[^"\s])? — and those can stay inline where they lived. The rest of the hoisting (amsmath/anchors/dollarmath/tasklists) can come back out.

On tests/test_redos.py, though, I don't think it's a strict "keep the globals or delete it" — there are ways to keep fuzzing with lazy compilation:

  • Behavioral (my preference): drive each plugin end-to-end — MarkdownIt().use(plugin) then md.render(<adversarial payload>) under a @pytest.mark.timeout. It doesn't care how or where the regexes are compiled, and it exercises the real attacker-controlled surface (the markdown source, plus the parser re-invoking rules across offsets — the O(n²) amplification you noted as Typo in Github "about" text #2). An isolated-regex check can both miss that and false-positive on patterns that adversarial input never actually reaches. The cost is maintaining a small payload per plugin.
  • AST walk if you want the zero-maintenance automatic coverage — more code, but it fully decouples the guard from the module structure instead of forcing globals.

If either feels like too much for this PR, I'm also happy to drop test_redos.py here and land just the pattern fixes, then add a guard back separately. Your call on which you'd rather own — I lean behavioral since it maps to how the code is actually attacked.


Generated by Claude Code

@KyleKing KyleKing left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented the behavioral change, which I agree is a better approach overall and rolled back the now unnecessary eager compilation. I'll fix CI and then mark ready for re-review

Comment thread tests/test_admon.py Outdated
@KyleKing
KyleKing requested a review from chrisjsewell July 21, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants