aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2026-09-12 09:12:13 +0100
committerMichael Williamson <mike@zwobble.org>2026-09-12 09:17:30 +0100
commitd05095fdc8608cb47844a2594e0480c04304fb89 (patch)
tree3b77a86d4355a5baf830e6b73f873d9013980db9
parent5f87fadfd8b7787b92b41f84ebf850dbe3a6cf28 (diff)
Avoid excessive backtracking when parsing unterminated strings
-rw-r--r--NEWS9
-rw-r--r--mammoth/styles/parser/tokeniser.py8
-rw-r--r--tests/styles/parser/tokeniser_tests.py14
3 files changed, 26 insertions, 5 deletions
diff --git a/NEWS b/NEWS
index cce165e..8b88dd9 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,12 @@
+# 1.12.2
+
+* Avoid excessive backtracking when parsing an unterminated string with many
+ escape sequences. The previous behaviour would allow maliciously crafted
+ documents to cause a denial of service.
+
+ Note that it is still strongly recommended to process untrusted documents in
+ a separate thread with a timeout to avoid potential similar issues.
+
# 1.12.1
* Fix: on Windows, when an image's content type includes a backslash in the
diff --git a/mammoth/styles/parser/tokeniser.py b/mammoth/styles/parser/tokeniser.py
index 7366cfe..96c537e 100644
--- a/mammoth/styles/parser/tokeniser.py
+++ b/mammoth/styles/parser/tokeniser.py
@@ -13,13 +13,13 @@ class TokenType(object):
UNTERMINATED_STRING = "unterminated string"
INTEGER = "integer"
END = "end"
-
+
def regex_tokeniser(rules):
rules = [(token_type, _to_regex(regex)) for token_type, regex in rules]
rules.append(("unknown", re.compile(".")))
-
+
def tokenise(value):
tokens = []
index = 0
@@ -39,7 +39,7 @@ def regex_tokeniser(rules):
return tokens
return tokenise
-
+
def _to_regex(value):
if hasattr(value, "match"):
@@ -48,7 +48,7 @@ def _to_regex(value):
return re.compile(value)
-_string_prefix = r"'(?:\\.|[^'])*"
+_string_prefix = r"'(?:\\(?:.|$)|[^'\\])*"
_identifier_character = r"(?:[a-zA-Z\-_]|\\.)"
tokenise = regex_tokeniser([
diff --git a/tests/styles/parser/tokeniser_tests.py b/tests/styles/parser/tokeniser_tests.py
index 482c5b0..9fce077 100644
--- a/tests/styles/parser/tokeniser_tests.py
+++ b/tests/styles/parser/tokeniser_tests.py
@@ -31,14 +31,26 @@ def test_strings_are_tokenised():
assert_tokens("'Tristan'", is_token("string", "'Tristan'"))
-def test_escape_sequences_in_strings_are_tokenised():
+def test_escaped_string_terminators_in_strings_are_tokenised():
assert_tokens(r"'Tristan\''", is_token("string", r"'Tristan\''"))
+def test_escape_sequences_in_strings_are_tokenised():
+ assert_tokens(r"'Tristan\\'", is_token("string", r"'Tristan\\'"))
+
+
def test_unterminated_strings_are_tokenised():
assert_tokens("'Tristan", is_token("unterminated string", "'Tristan"))
+def test_unterminated_strings_with_unterminated_escape_are_tokenised():
+ assert_tokens("'Tristan\\", is_token("unterminated string", "'Tristan\\"))
+
+
+def test_unterminated_strings_with_many_escape_sequences_do_not_cause_excessive_backtracking():
+ assert_tokens("'" + "\\a" * 50, is_token("unterminated string", "'" + "\\a" * 50))
+
+
def test_arrows_are_tokenised():
assert_tokens("=>=>", is_token("symbol", "=>"), is_token("symbol", "=>"))