Skip to content

2.7.18.16: csv.Sniffer quadratic regex on quoted fields (CVE-2026-18503) - #93

Merged
icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-csv-sniffer
Oct 28, 2026
Merged

icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-csv-sniffer

Conversation

@icanhasmath

Copy link
Copy Markdown

Backport of python/cpython pythongh-98820 (CVE-2026-18503, PSF-2026-35) to 2.7.18.x.

Problem: Sniffer._guess_quote_and_delimiter() matched a quoted field body with a lazy .*?. That scans to the end of the sample from every start position, so the cost is quadratic in the sample size.

Fix: a quoted field body now ends at the first quote that isn't doubled, as it does for a csv reader. This changes some sniffing results on messy samples; the change is upstream's.

2.7 adaptation: Python 2.7's re has no possessive *+, so the body is matched atomically using (?=(?P<body>…*))(?P=body). Under Python 3.12 I fuzzed 20,000 random samples, and this pattern gives the same matches as upstream's possessive pattern on every one.

Test: TestSniffer.test_sniff_quoted_single_column. Without the fix this single test takes 28.7s; with it, all of test_csv passes in 1.3s.

Part of 2.7.18.16. It targets the 2.7.18.16 integration branch, like #92.

…VE-2026-18503)

Backport of python/cpython pythongh-98820 (3.12 063d455).

Sniffer._guess_quote_and_delimiter() matched a quoted field body with a
lazy ".*?", which scans to the end of the sample from every start
position: quadratic in the sample size.  An attacker-controlled sample
passed to Sniffer.sniff() could burn significant CPU.  The body of a
quoted field now ends at the first quote that is not doubled, as it does
for a csv reader.

2.7.18.x: Python 2.7's re module has no possessive quantifiers (*+ is
3.11+), so the body is matched atomically with the equivalent
lookahead + backreference idiom (?=(?P<body>...*))(?P=body).  A
differential fuzz of 20000 random samples under Python 3.12 found the
2.7 pattern and upstream's possessive pattern produce identical matches.
assertRaisesRegexp instead of assertRaisesRegex in the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@ezequielp-activestate ezequielp-activestate 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.

No blockers.

  • Fix matches upstream. The lookahead-plus-backreference body is an exact atomic-group emulation of upstream's possessive (?:(?P=quote){2}|(?!(?P=quote)).)*+ (pythongh-98820, b30c7fa). The rest of the pattern is unchanged from upstream, and findall's extra body group is harmless because the code indexes groups through groupindex.

  • Repro: the upstream test sample ('"abcdefghijklmnopqrstuvwxyz"\n' * n, delimiters=',:|\t'):

    n base this branch
    1,000 0.49 s 0.10 s
    2,000 1.97 s 0.18 s
    4,000 7.40 s 0.38 s
  • No regressions spotted: normal and doubled-quote samples sniff the same as before, and a single 2 MB quoted field still sniffs (0.7 s, no recursion error).

  • Tests: test_csv passes.

  • FYI, not for this PR: the doublequote check in the same function (dq_regexp) is still quadratic. '",' * 20000 takes about 9 s on both base and this branch, and upstream 3.14 shows the same timing for that regex, so it isn't a porting gap.

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