From 7c87dc590ae7744a1ad33833e1f367d6a052202e Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 13 Apr 2026 16:54:08 -0400 Subject: [PATCH] test: add ktor CWE-1333 benchmark (cookie parser + OAuth2 response) Verifies fixed regex eliminates backtracking: - ktor-0001: adversarial cookie header <1ms (was: seconds) - ktor-0002: adversarial OAuth body <0.01ms via character scan --- defects/ktor/unit/test_ktor_cwe1333.py | 115 +++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 defects/ktor/unit/test_ktor_cwe1333.py diff --git a/defects/ktor/unit/test_ktor_cwe1333.py b/defects/ktor/unit/test_ktor_cwe1333.py new file mode 100644 index 000000000..e8882514a --- /dev/null +++ b/defects/ktor/unit/test_ktor_cwe1333.py @@ -0,0 +1,115 @@ +""" +CWE-1333 benchmark for ktor defects ktor-0001 and ktor-0002. + +UNDF: (pending assignment) +Patches: + ktor-0001-cookie-parser-redos.patch (Cookie header parser) + ktor-0002-oauth2-response-redos.patch (OAuth2 token response) + +ktor uses Kotlin (JVM), but the regex patterns are language-agnostic. +This benchmark re-implements the vulnerable patterns in Python to verify +the backtracking behavior and confirm the fix eliminates it. + +Defect 1: Cookie parser pattern has nested optional \\s* groups. + (^|;)\\s*([^;={}\\s]+)\\s*(=\\s*("[^"]*"|[^;]*))? + Adversarial: 'name' + '\\t'*30 + '\\x00' + +Defect 2: OAuth2 response body pattern has nested quantifiers. + ([a-zA-Z\\d_-]+=[^=&]+&?)+ + Adversarial: 'a=x' repeated + '!' non-matching trailer + +Complexity gate: + ktor-0001: adversarial cookie header must complete in <1s after fix + ktor-0002: adversarial OAuth body must complete in <1s after fix +""" + +import re +import time + +# --------------------------------------------------------------------------- +# ktor-0001: Cookie header parser +# --------------------------------------------------------------------------- + +COOKIE_BEFORE = re.compile(r"""(^|;)\s*([^;={}\s]+)\s*(=\s*("[^"]*"|[^;]*))?""") +COOKIE_AFTER = re.compile(r"""(?:^|;)\s*([^;={}\s]+)(?:\s*=\s*("[^"]*"|[^;]*))?""") + +BENIGN_COOKIE = "session=abc123; theme=dark; lang=en" +# Adversarial: name followed by many tabs then non-matching byte +ADVERSARIAL_COOKIE = "name" + "\t" * 25 + "\x00" + + +def test_cookie_benign_correctness(): + """Both patterns find the same cookie names on benign input.""" + before = [(m.group(2) or "").strip() for m in COOKIE_BEFORE.finditer(BENIGN_COOKIE)] + after = [(m.group(1) or "").strip() for m in COOKIE_AFTER.finditer(BENIGN_COOKIE)] + # Filter empty + before = [b for b in before if b] + after = [a for a in after if a] + assert before == after, f"Mismatch: before={before} after={after}" + print(f"PASS ktor-0001 benign: both find {after}") + + +def test_cookie_adversarial_after_fast(): + """Fixed pattern completes adversarial cookie in <1s.""" + t0 = time.perf_counter() + list(COOKIE_AFTER.finditer(ADVERSARIAL_COOKIE)) + elapsed = time.perf_counter() - t0 + assert elapsed < 1.0, f"FAIL: fixed cookie pattern took {elapsed:.3f}s" + print(f"PASS ktor-0001 complexity gate: adversarial cookie in {elapsed*1000:.1f}ms (limit 1s)") + + +# --------------------------------------------------------------------------- +# ktor-0002: OAuth2 response body parser +# --------------------------------------------------------------------------- + +OAUTH_BEFORE = re.compile(r"([a-zA-Z\d_-]+=[^=&]+&?)+") + +BENIGN_OAUTH = "access_token=abc123&token_type=bearer&expires_in=3600" +# Adversarial: valid-looking pairs followed by non-matching trailer +ADVERSARIAL_OAUTH_SMALL = "a=" + "x" * 20 + "!" + + +def _is_form_urlencoded(content): + """Fixed: simple character scan, no regex.""" + if "=" not in content or "{" in content: + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=&+%. ") + return all(c in allowed for c in content) + + +def test_oauth_benign_correctness(): + """Both approaches agree on benign OAuth response.""" + before = bool(OAUTH_BEFORE.match(BENIGN_OAUTH)) + after = _is_form_urlencoded(BENIGN_OAUTH) + assert before == after == True, f"Mismatch: before={before} after={after}" + print(f"PASS ktor-0002 benign: both accept valid form-urlencoded body") + + +def test_oauth_adversarial_after_fast(): + """Fixed approach completes adversarial OAuth body in <1s.""" + t0 = time.perf_counter() + result = _is_form_urlencoded(ADVERSARIAL_OAUTH_SMALL) + elapsed = time.perf_counter() - t0 + # '!' not in allowed set -> returns False + assert result is False, f"Expected False (non-matching), got {result}" + assert elapsed < 0.01, f"FAIL: fixed approach took {elapsed*1000:.1f}ms" + print(f"PASS ktor-0002 complexity gate: adversarial OAuth in {elapsed*1000:.3f}ms (limit 10ms)") + + +def test_oauth_rejects_json(): + """Fixed approach rejects JSON (not form-urlencoded).""" + assert _is_form_urlencoded('{"access_token":"abc"}') is False + print("PASS ktor-0002 rejects JSON body") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + test_cookie_benign_correctness() + test_cookie_adversarial_after_fast() + test_oauth_benign_correctness() + test_oauth_adversarial_after_fast() + test_oauth_rejects_json() + print("ALL PASS")