nltk--nltk
3454a55636
cffconvert / validate (push) Has been cancelled
ci-workflow / pre-commit (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (macos-latest) (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (ubuntu-latest) (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (windows-latest) (push) Has been cancelled
ci-workflow / Python 3.10 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.11 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.12 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.13 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.14 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.14t on macos-latest (push) Has been cancelled
ci-workflow / Python 3.10 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.11 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.12 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.13 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.14 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.14t on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.10 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.11 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.12 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.13 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.14 on windows-latest (push) Has been cancelled
95 行
3.7 KiB
Python
95 行
3.7 KiB
Python
"""Regression tests for ReDoS in ReviewsCorpusReader (CWE-1333).
|
|
|
|
The ``FEATURES`` regex extracts ``feature[+N]`` annotations from each review
|
|
line. With an unbounded feature label, ``re.findall`` rescans a long
|
|
bracket-less line quadratically, so a crafted corpus line can hang the reader.
|
|
The label length is now bounded, making extraction linear.
|
|
|
|
The "must not hang" tests run the work in a separate process (spawn) with a
|
|
hard timeout and ``terminate()`` on overrun, so a regression to a quadratic
|
|
regex cannot keep burning CPU for the rest of the suite, and any exception in
|
|
the worker is propagated back to the assertions instead of being swallowed.
|
|
"""
|
|
|
|
import multiprocessing
|
|
import queue
|
|
|
|
from nltk.corpus.reader.reviews import FEATURES, ReviewsCorpusReader
|
|
|
|
# A long, bracket-less word run: ~250 KB. Linear with the bounded regex
|
|
# (milliseconds); ~quadratic and ~50 s with the old unbounded one.
|
|
_CRAFTED_LINE = "word " * 50_000
|
|
# Generous vs. the linear cost (which is ~ms after process startup), but far
|
|
# below the quadratic regression cost, so a regression fails fast and cleanly.
|
|
_TIMEOUT = 15
|
|
|
|
|
|
def _features_worker(result_q):
|
|
try:
|
|
result_q.put(("ok", FEATURES.findall(_CRAFTED_LINE)))
|
|
except BaseException as exc: # surface to the parent process
|
|
result_q.put(("error", repr(exc)))
|
|
|
|
|
|
def _reader_worker(result_q, root, fileid):
|
|
try:
|
|
reviews = ReviewsCorpusReader(root, fileid).reviews()
|
|
# Return only picklable data: number of reviews and the first review's
|
|
# features (a list of (feature, score) string tuples).
|
|
result_q.put(("ok", (len(reviews), reviews[0].features())))
|
|
except BaseException as exc:
|
|
result_q.put(("error", repr(exc)))
|
|
|
|
|
|
def _run_in_process(target, args=()):
|
|
"""Run ``target(result_q, *args)`` in a spawned process with a timeout.
|
|
|
|
Returns ``(finished, status, payload)``. If the worker overruns ``_TIMEOUT``
|
|
it is terminated (no lingering CPU) and ``finished`` is ``False``.
|
|
"""
|
|
ctx = multiprocessing.get_context("spawn")
|
|
result_q = ctx.Queue()
|
|
proc = ctx.Process(target=target, args=(result_q, *args))
|
|
proc.start()
|
|
proc.join(_TIMEOUT)
|
|
if proc.is_alive():
|
|
proc.terminate()
|
|
proc.join()
|
|
return False, None, None
|
|
try:
|
|
status, payload = result_q.get_nowait()
|
|
except queue.Empty:
|
|
return True, "error", "worker produced no result"
|
|
return True, status, payload
|
|
|
|
|
|
def test_features_regex_preserves_normal_extraction():
|
|
"""The bounded regex must extract the same features as before on real data."""
|
|
line = "battery life[+2] and the zoom[-1] are ok"
|
|
assert FEATURES.findall(line) == [("battery life", "+2"), ("and the zoom", "-1")]
|
|
assert FEATURES.findall("size[+3]") == [("size", "+3")]
|
|
assert FEATURES.findall("no feature annotation here") == []
|
|
|
|
|
|
def test_features_regex_is_linear_on_crafted_line():
|
|
"""A long, bracket-less word run must not blow up (ReDoS)."""
|
|
finished, status, payload = _run_in_process(_features_worker)
|
|
assert finished, "FEATURES regex hung on a crafted line (ReDoS)"
|
|
assert status == "ok", f"worker raised: {payload}"
|
|
assert payload == []
|
|
|
|
|
|
def test_reviews_reader_does_not_hang_on_crafted_corpus(tmp_path):
|
|
"""End-to-end: reading a malicious review file must terminate and succeed."""
|
|
(tmp_path / "r.txt").write_text("[t]title\n" + _CRAFTED_LINE + "\n")
|
|
|
|
finished, status, payload = _run_in_process(
|
|
_reader_worker, (str(tmp_path), "r.txt")
|
|
)
|
|
assert finished, "ReviewsCorpusReader hung on a crafted corpus line (ReDoS)"
|
|
assert status == "ok", f"reader raised in worker: {payload}"
|
|
num_reviews, features = payload
|
|
# The call actually succeeded (output populated), not silently swallowed.
|
|
assert num_reviews == 1
|
|
assert features == []
|