Skip to content

fix: avoid catastrophic backtracking in number parsing for empty thousand separator (DEV-2120) - #1713

Open
marcin-kordas-hoc wants to merge 1 commit into
developfrom
fix/dev-2120-redos-number-parsing
Open

fix: avoid catastrophic backtracking in number parsing for empty thousand separator (DEV-2120)#1713
marcin-kordas-hoc wants to merge 1 commit into
developfrom
fix/dev-2120-redos-number-parsing

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Entering a long run of digits ending in a non-digit character (e.g. 012345678901234567890123456789012345678901234567890123456789a) into a cell froze the page when formulas were enabled. DEV-2120 / HOT-9767.

Root cause

NumberLiteralHelper builds its number-detection pattern by interpolating the configured separators. With the default thousandSeparator: '', the group (${thousandSeparator}\d{3,})* degenerates to (\d{3,})* placed immediately after \d+:

^([+-]?((\.\d+)|(\d+(\d{3,})*(\.\d*)?)))([eE][+-]?\d+)?$
             └──── nested quantifiers on the same class ────┘

For a long digit run that ultimately fails to match (trailing non-digit), the engine explores exponentially many ways to partition the digits between \d+ and the repeated \d{3,} group — classic catastrophic backtracking (ReDoS). Parse time roughly doubles every ~2 characters, so a 60-character input never returns.

The same pattern is reached from raw cell input (CellContentParser) and from string→number coercion during formula evaluation (ArithmeticHelper), so =VALUE("…") and arithmetic over such text hung too. Fixing the pattern builder covers all entry points.

Fix

Omit the thousand-separator group entirely when the separator is empty. The emitted pattern for a non-empty separator (,, , .) is byte-for-byte unchanged — a literal separator is a mandatory anchor between repetitions, so no ambiguous partition exists and those configs were never vulnerable.

Testing

Paired tests in handsontable/hyperformula-tests (branch fix/dev-2120-redos-number-parsing):

  • white-box guard that the default-config pattern contains no nested digit quantifier (deterministic regression tripwire — a synchronous ReDoS cannot be caught by a per-test timeout);
  • behavioral coverage: trailing letter, trailing non-letter symbol, separator matrix, long-integer value fidelity;
  • end-to-end via setCellContents (raw, percent, currency) and formula coercion (=VALUE(...)).

Reviewer notes

  • Why the white-box test (asserting numberPattern.source has no (\d{3,})*): a synchronous ReDoS cannot be caught by a Jest/Jasmine per-test timeout — the timer can't fire while the regex is stuck on the main thread — so asserting the emitted pattern shape is the one deterministic regression tripwire. The behavioral/e2e tests still cover actual behavior.
  • Why no input-length cap: the fix sits in the pattern builder, so it covers every entry point at once, and non-empty separators are provably linear (the literal separator anchors each repetition). A length cap would be complementary defense-in-depth — deliberately left out to keep this fix focused on the root cause.

Notes

Long-standing issue (reproduced on docs v17.1 and v18.0), not a v18 regression.

🤖 Generated with Claude Code

@netlify

netlify Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy Preview for hyperformula-dev-docs ready!

Name Link
🔨 Latest commit 8ec26f4
🔍 Latest deploy log https://app.netlify.com/projects/hyperformula-dev-docs/deploys/6a5f8858a31374000860892b
😎 Deploy Preview https://deploy-preview-1713--hyperformula-dev-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@qunabu

qunabu commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Task linked: HF-154 Agent-friendly docs

Comment thread scripts/extract-public-api.js Outdated
if (!paramName) continue
// Optional when the name fragment contains `?` or a default `=` precedes `:`.
const beforeColon = clean.split(':')[0] ?? ''
const isOptional = beforeColon.includes('?') || beforeColon.includes('=')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional params marked required

Medium Severity

parseParams only treats a parameter as optional when ? or = appears before the :. Typical TypeScript defaults like name: Type = value put = after the type, so core factory methods such as buildFromArray and buildEmpty are exported with optional parameters classified as required.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6637a91. Configure here.

@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the fix/dev-2120-redos-number-parsing branch from 6637a91 to 2f1ca3a Compare July 21, 2026 14:25
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Performance comparison of head (8ec26f4) vs base (fd04f77)

                                     testName |   base |   head | change
------------------------------------------------------------------------
                                      Sheet A | 515.07 | 508.66 | -1.24%
                                      Sheet B | 169.28 |  167.1 | -1.29%
                                      Sheet T | 145.91 | 144.13 | -1.22%
                                Column ranges | 529.37 | 533.49 | +0.78%
Sheet A:  change value, add/remove row/column |  18.95 |  17.72 | -6.49%
 Sheet B: change value, add/remove row/column | 153.86 | 152.87 | -0.64%
                   Column ranges - add column | 167.64 | 168.84 | +0.72%
                Column ranges - without batch | 510.18 | 509.34 | -0.16%
                        Column ranges - batch | 132.11 | 133.83 | +1.30%

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2f1ca3a. Configure here.

Comment thread docs/.vuepress/plugins/md-companions/strip.js Outdated
…sand separator (DEV-2120)

When thousandSeparator is empty (the default), the number-detection pattern's
group `(sep\d{3,})*` degenerated to `(\d{3,})*` adjacent to `\d+`, producing
catastrophic regex backtracking (ReDoS) on a long run of digits ending in a
non-digit character. Entering such a value into a cell froze the page.

Omit the thousand-separator group entirely when the separator is empty. Behavior
for non-empty separators is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the fix/dev-2120-redos-number-parsing branch from 2f1ca3a to 8ec26f4 Compare July 21, 2026 14:55
@marcin-kordas-hoc
marcin-kordas-hoc requested a review from sequba July 21, 2026 17:01
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.17%. Comparing base (fd04f77) to head (8ec26f4).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop    #1713   +/-   ##
========================================
  Coverage    97.17%   97.17%           
========================================
  Files          176      176           
  Lines        15483    15484    +1     
  Branches      3429     3430    +1     
========================================
+ Hits         15045    15046    +1     
  Misses         438      438           
Files with missing lines Coverage Δ
src/NumberLiteralHelper.ts 94.11% <100.00%> (+0.36%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread CHANGELOG.md

- Fixed the behavior of `MATCH`, `VLOOKUP`, `HLOOKUP`, and `XLOOKUP` functions when the search range contained empty cells. [#1697](https://github.com/handsontable/hyperformula/pull/1697)
- Fixed the `VLOOKUP`, `HLOOKUP`, and `XLOOKUP` functions to return `0` instead of an empty value when the matched cell in the result range is empty. [#1697](https://github.com/handsontable/hyperformula/pull/1697)
- Fixed the page freezing when entering a long string of digits ending in a non-digit character (e.g. `012...789a`) into a cell, caused by catastrophic regex backtracking in number parsing. [#1713](https://github.com/handsontable/hyperformula/pull/1713)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reference this task in changelog

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.

3 participants