Fix quadratic ReDoS in PEM import block scanning

Replace the backtracking _PEM_BLOCK regex with a forward-only scanner
that indexes END markers by label, so repeated unterminated headers do
not cause the regex to rescan the remaining input.

Impact: 80 KiB payload went from 8.34 s to 0.001 s.
This commit is contained in:
2026-10-26 12:06:15 +08:00
parent 718d11b4c0
commit b59b935d1b
2 changed files with 56 additions and 9 deletions
@@ -1,6 +1,7 @@
"""Core domain tests: names, extensions, keys, certificates, CRLs and exports."""
from __future__ import annotations
import time
import unittest
from datetime import datetime, timedelta, timezone
@@ -273,6 +274,30 @@ class ExportImportTest(unittest.TestCase):
items = imports.parse_file(pem.data, "export.crt")
self.assertEqual(items[0].kind, "certificate")
def test_pem_bundle_ignores_incomplete_blocks(self) -> None:
certificate = exports.export_certificate(self.cert, "pem", name="export").data
private_key = keys.private_pem(self.key)
pem = (
b"before\n-----BEGIN INCOMPLETE-----\nignored\n"
+ certificate
+ b"\n"
+ private_key
+ b"\nafter\n"
)
items = imports.parse_pem_bundle(pem)
self.assertEqual([item.kind for item in items], ["certificate", "key"])
mismatched = b"-----BEGIN CERTIFICATE-----\nignored\n-----END PRIVATE KEY-----"
self.assertEqual(imports.parse_pem_bundle(mismatched), [])
def test_unterminated_pem_headers_complete_quickly(self) -> None:
header = b"-----BEGIN UNTERMINATED-----\n"
data = header * ((80 * 1024) // len(header))
started = time.perf_counter()
items = imports.parse_pem_bundle(data)
elapsed = time.perf_counter() - started
self.assertEqual(items, [])
self.assertLess(elapsed, 2.0, f"PEM scanning took {elapsed:.3f}s for {len(data)} bytes")
def test_key_exports(self) -> None:
for fmt in ("pem-public", "pem-private", "pkcs8", "ssh-public", "jwk-public", "pvk"):
result = exports.export_key(self.key, fmt, name="export")