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
This commit is contained in:
russell@unturf.com 2026-04-13 16:54:08 -04:00
parent d1f82fd8e3
commit 7c87dc590a

View file

@ -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")