81 lines
3.8 KiB
Python
81 lines
3.8 KiB
Python
import http.client
|
|
|
|
from run_network_tests import TestFailure
|
|
|
|
|
|
def _direct_http_request(path, headers=None):
|
|
connection = http.client.HTTPConnection("127.0.0.1", 8080, timeout=5.0)
|
|
try:
|
|
connection.request("GET", path, headers=headers or {})
|
|
response = connection.getresponse()
|
|
body = response.read().decode("utf-8", errors="replace")
|
|
return response.status, dict(response.getheaders()), body
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
def _frontend_http_request(path, headers=None):
|
|
request_headers = {"Host": "uce.openfu.com"}
|
|
request_headers.update(headers or {})
|
|
connection = http.client.HTTPConnection("127.0.0.1", 80, timeout=5.0)
|
|
try:
|
|
connection.request("GET", path, headers=request_headers)
|
|
response = connection.getresponse()
|
|
body = response.read().decode("utf-8", errors="replace")
|
|
return response.status, response.getheaders(), body
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
def register(registry):
|
|
def direct_http_rejects_dotdot(context):
|
|
status, headers, body = _direct_http_request("/../site/demo/hello.uce")
|
|
if status == 200 or "hello world" in body:
|
|
raise TestFailure("direct HTTP accepted dot-dot script traversal")
|
|
return "direct HTTP dot-dot traversal rejected with HTTP %s" % status
|
|
|
|
def direct_http_ignores_script_filename_header(context):
|
|
status, headers, body = _direct_http_request(
|
|
"/no-such-script.uce",
|
|
headers={"Script-Filename": "/Code/uce.openfu.com/uce/site/demo/hello.uce"},
|
|
)
|
|
if status == 200 or "hello world" in body:
|
|
raise TestFailure("direct HTTP trusted client Script-Filename header")
|
|
return "direct HTTP Script-Filename override rejected with HTTP %s" % status
|
|
|
|
def response_headers_are_sanitized(context):
|
|
response = context.request("/tests/security_headers.uce")
|
|
injected_headers = [
|
|
"X-UCE-Injected",
|
|
"X-UCE-Injected-Name",
|
|
"X-UCE-Cookie-Injected",
|
|
"X-UCE-Redirect-Injected",
|
|
"X-UCE-Status-Injected",
|
|
]
|
|
for header in injected_headers:
|
|
if header in response.headers:
|
|
raise TestFailure("CRLF header injection produced extra response header %s" % header)
|
|
location = response.headers.get("Location", "")
|
|
if "\r" in location or "\n" in location:
|
|
raise TestFailure("Location header contains raw CR/LF")
|
|
status, direct_headers, direct_body = _direct_http_request("/site/tests/security_headers.uce")
|
|
if status >= 500 and "security header sanitizer test" not in direct_body:
|
|
raise TestFailure("direct HTTP sanitizer test did not render successfully; status=%s" % status)
|
|
return "CRLF response header injection was sanitized"
|
|
|
|
def unknown_session_id_is_not_adopted(context):
|
|
attacker_id = "a" * 64
|
|
status, headers, body = _frontend_http_request("/tests/http.uce", headers={"Cookie": "uce-site-tests=" + attacker_id})
|
|
set_cookies = [value for name, value in headers if name.lower() == "set-cookie"]
|
|
session_cookies = [value for value in set_cookies if value.startswith("uce-site-tests=")]
|
|
if any(("uce-site-tests=" + attacker_id) in value for value in session_cookies):
|
|
raise TestFailure("session_start adopted caller supplied unknown session id")
|
|
if not session_cookies or not any("HttpOnly" in value and "SameSite=Lax" in value for value in session_cookies):
|
|
raise TestFailure("session_start did not issue a hardened replacement session cookie")
|
|
return "unknown caller-supplied session id was replaced"
|
|
|
|
registry.case("direct HTTP rejects dot-dot script traversal", direct_http_rejects_dotdot, tags=["security", "http", "internal"])
|
|
registry.case("direct HTTP ignores Script-Filename header", direct_http_ignores_script_filename_header, tags=["security", "http", "internal"])
|
|
registry.case("response headers sanitize CRLF", response_headers_are_sanitized, tags=["security", "http", "internal"])
|
|
registry.case("unknown session ids are not adopted", unknown_session_id_is_not_adopted, tags=["security", "http", "internal"])
|