From 84a615ee413c47b4b8579df7585a424f7d7b1ff3 Mon Sep 17 00:00:00 2001 From: Brian Willows Date: Mon, 21 Sep 2026 13:53:16 +0100 Subject: [PATCH] fix: avoid quadratic-time PEM post-boundary check decode() verified the post-encapsulation boundary with re.search(r"-----END (.*)-----\s*$"). Every "-----END " in the input is a start position for that search, and the greedy (.*) backtracks the whole remainder at each one, so the cost is quadratic in len(pem_data). PEM.decode is reached from RSA.import_key, ECC.import_key, DSA.import_key and PKCS8, with no size limit on that path, so an application importing a user-supplied key or certificate reaches it with attacker-controlled length. On 3.23.0 a 128 KB input costs 10,179 ms through RSA.import_key, against 0.4 ms for a valid PEM of the same size. The marker is already known from the pre-boundary match, so the boundary can be compared directly and no search is needed. This also makes the m.group(1) != marker comparison redundant. After the change the same 128 KB input costs 0.2 ms. Behaviour is unchanged: a 12-case differential test over CRLF, missing trailing newline, trailing whitespace and junk, marker mismatch, absent boundary, hyphenated markers, RSA PRIVATE KEY, ENCRYPTED PRIVATE KEY, EC PARAMETERS and a mid-document -----END reports no disagreements, and test_PKCS8, test_PBES and the five key-import suites give 91 passed before and after. --- lib/Crypto/IO/PEM.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Crypto/IO/PEM.py b/lib/Crypto/IO/PEM.py index 9b8b0710e..f32505c3c 100644 --- a/lib/Crypto/IO/PEM.py +++ b/lib/Crypto/IO/PEM.py @@ -129,10 +129,12 @@ def decode(pem_data, passphrase=None): raise ValueError("Not a valid PEM pre boundary") marker = m.group(1) - # Verify Post-Encapsulation Boundary - r = re.compile(r"-----END (.*)-----\s*$") - m = r.search(pem_data) - if not m or m.group(1) != marker: + # Verify Post-Encapsulation Boundary. + # The marker is already known from the pre-boundary, so the boundary can be + # matched directly. Searching for it with r"-----END (.*)-----\s*$" was + # quadratic in len(pem_data): every "-----END " is a start position for the + # search, and the greedy (.*) backtracks the whole remainder at each one. + if not pem_data.rstrip().endswith("-----END %s-----" % marker): raise ValueError("Not a valid PEM post boundary") # Removes spaces and slit on lines