diff --git a/octopoes/bits/check_csp_header/bit.py b/octopoes/bits/check_csp_header/bit.py index 72bc8be7bc4..73b0c268801 100644 --- a/octopoes/bits/check_csp_header/bit.py +++ b/octopoes/bits/check_csp_header/bit.py @@ -1,9 +1,11 @@ -from bits.definitions import BitDefinition -from octopoes.models.types import HTTPHeader +from bits.definitions import BitDefinition, BitParameterDefinition +from octopoes.models.ooi.web import HTTPHeader, HTTPResource BIT = BitDefinition( id="check-csp-header", - consumes=HTTPHeader, - parameters=[], + consumes=HTTPResource, + parameters=[ + BitParameterDefinition(ooi_type=HTTPHeader, relation_path="resource"), + ], module="bits.check_csp_header.check_csp_header", ) diff --git a/octopoes/bits/check_csp_header/check_csp_header.py b/octopoes/bits/check_csp_header/check_csp_header.py index c621a652e74..0348f203de2 100644 --- a/octopoes/bits/check_csp_header/check_csp_header.py +++ b/octopoes/bits/check_csp_header/check_csp_header.py @@ -5,19 +5,45 @@ from octopoes.models import OOI, Reference from octopoes.models.ooi.findings import Finding, KATFindingType +from octopoes.models.ooi.web import HTTPResource from octopoes.models.types import HTTPHeader NON_DECIMAL_FILTER = re.compile(r"[^\d.]+") +XSS_CAPABLE_TYPES = [ + "text/html", + "application/xhtml+xml", + "application/xml", + "text/xml", + "image/svg+xml", +] -def run(input_ooi: HTTPHeader, additional_oois: list, config: dict[str, Any]) -> Iterator[OOI]: - header = input_ooi - if header.key.lower() != "content-security-policy": + +def is_xss_capable(content_type: str) -> bool: + """Determine if the content type indicates XSS capability.""" + main_type = content_type.split(";")[0].strip().lower() + return main_type in XSS_CAPABLE_TYPES + + +def run(resource: HTTPResource, additional_oois: list[HTTPHeader], config: dict[str, Any]) -> Iterator[OOI]: + if not additional_oois: + return + + headers = {header.key.lower(): header.value for header in additional_oois} + + content_type = headers.get("content-type", "") + # if no content type is present, we can't determine if the resource is XSS capable, so assume it is + if content_type and not is_xss_capable(content_type): + return + + csp_header = headers.get("content-security-policy", "") + + if not csp_header: return findings: list[str] = [] - if "http://" in header.value: + if "http://" in csp_header: findings.append("Http should not be used in the CSP settings of an HTTP Header.") # checks for a wildcard in domains in the header @@ -26,30 +52,30 @@ def run(input_ooi: HTTPHeader, additional_oois: list, config: dict[str, Any]) -> # 3: second-level domain # 4: end with either a space, a ';', a :port or the end of the string # {1}{ 2}{ 3 }{ 4 } - if re.search(r"\S+\*\.\S{2,3}([\s]+|$|;|:[0-9]+)", header.value): + if re.search(r"\S+\*\.\S{2,3}([\s]+|$|;|:[0-9]+)", csp_header): findings.append("The wildcard * for the scheme and host part of any URL should never be used in CSP settings.") - if "unsafe-inline" in header.value or "unsafe-eval" in header.value or "unsafe-hashes" in header.value: + if "unsafe-inline" in csp_header or "unsafe-eval" in csp_header or "unsafe-hashes" in csp_header: findings.append( "unsafe-inline, unsafe-eval and unsafe-hashes should not be used in the CSP settings of an HTTP Header." ) - if "frame-src" not in header.value and "default-src" not in header.value and "child-src" not in header.value: + if "frame-src" not in csp_header and "default-src" not in csp_header and "child-src" not in csp_header: findings.append("frame-src has not been defined or does not have a fallback.") - if "script-src" not in header.value and "default-src" not in header.value: + if "script-src" not in csp_header and "default-src" not in csp_header: findings.append("script-src has not been defined or does not have a fallback.") - if "base-uri" not in header.value: + if "base-uri" not in csp_header: findings.append("base-uri has not been defined, default-src does not apply.") - if "frame-ancestors" not in header.value: + if "frame-ancestors" not in csp_header: findings.append("frame-ancestors has not been defined.") - if "default-src" not in header.value: + if "default-src" not in csp_header: findings.append("default-src has not been defined.") - policies = [policy.strip().split(" ") for policy in header.value.split(";")] + policies = [policy.strip().split(" ") for policy in csp_header.split(";")] for policy in policies: if len(policy) < 2: findings.append("CSP setting has no value.") @@ -98,7 +124,7 @@ def run(input_ooi: HTTPHeader, additional_oois: list, config: dict[str, Any]) -> description += f"\n {index + 1}. {finding}" yield from _create_kat_finding( - header.reference, + resource.reference, kat_id="KAT-CSP-VULNERABILITIES", description=description, ) diff --git a/octopoes/bits/missing_headers/missing_headers.py b/octopoes/bits/missing_headers/missing_headers.py index b0c75045e15..bb8c6575baf 100644 --- a/octopoes/bits/missing_headers/missing_headers.py +++ b/octopoes/bits/missing_headers/missing_headers.py @@ -5,12 +5,27 @@ from octopoes.models.ooi.findings import Finding, KATFindingType from octopoes.models.ooi.web import HTTPHeader, HTTPResource +XSS_CAPABLE_TYPES = [ + "text/html", + "application/xhtml+xml", + "application/xml", + "text/xml", + "image/svg+xml", +] + + +def is_xss_capable(content_type: str) -> bool: + """Determine if the content type indicates XSS capability.""" + main_type = content_type.split(";")[0].strip().lower() + return main_type in XSS_CAPABLE_TYPES + def run(resource: HTTPResource, additional_oois: list[HTTPHeader], config: dict[str, Any]) -> Iterator[OOI]: if not additional_oois: return header_keys = [header.key.lower() for header in additional_oois] + headers = {header.key.lower(): header.value for header in additional_oois} if "location" in header_keys: return @@ -25,7 +40,7 @@ def run(resource: HTTPResource, additional_oois: list[HTTPHeader], config: dict[ ) yield finding - if "content-security-policy" not in header_keys: + if "content-security-policy" not in header_keys and is_xss_capable(headers.get("content-type", "")): ft = KATFindingType(id="KAT-NO-CSP") finding = Finding( finding_type=ft.reference, diff --git a/octopoes/tests/test_bit_csp_header.py b/octopoes/tests/test_bit_csp_header.py index 7b80287f754..ec6906a6b65 100644 --- a/octopoes/tests/test_bit_csp_header.py +++ b/octopoes/tests/test_bit_csp_header.py @@ -3,20 +3,33 @@ from octopoes.models.ooi.web import HTTPHeader -def test_https_hsts(http_resource_https): - results = [ - list(run(HTTPHeader(resource=http_resource_https.reference, key=key, value=value), [], {})) - for key, value in [ - ("Content-Type", "text/html"), - ("Content-security-poliCY", "text/html"), - ("content-security-policy", "http://abc.com"), - ("content-security-policy", "https://abc.com"), - ("content-security-policy", "https://*.com"), - ("content-security-policy", "https://a.com; ...; media-src 'self'; media-src 10.10.10.10;"), - ("content-security-policy", "unsafe-inline-uri * strict-dynamic; test http: 127.0.0.1"), - ] +def test_check_csp_headers(http_resource_https): + headers_to_test = [ + ("Content-Type", "text/html"), + ("Content-security-poliCY", "text/html"), + ("content-security-policy", "http://abc.com"), + ("content-security-policy", "https://abc.com"), + ("content-security-policy", "https://*.com"), + ("content-security-policy", "https://a.com; ...; media-src 'self'; media-src 10.10.10.10;"), + ("content-security-policy", "unsafe-inline-uri * strict-dynamic; test http: 127.0.0.1"), ] + results = [] + + # Iterate over headers and execute `run` function for each adding the content type header for xss capability check + for key, value in headers_to_test: + result = list( + run( + resource=http_resource_https, + additional_oois=[ + HTTPHeader(resource=http_resource_https.reference, key="Content-Type", value="text/html"), + HTTPHeader(resource=http_resource_https.reference, key=key, value=value), + ], + config={}, + ) + ) + results.append(result) + assert results[0] == [] assert len(results[1]) == 2 assert results[1][0].id == "KAT-CSP-VULNERABILITIES" @@ -99,3 +112,33 @@ def test_https_hsts(http_resource_https): 9. a blanket protocol source should not be used in the value of any type in the CSP settings. 10. Private, local, reserved, multicast, loopback ips should not be allowed in the CSP settings.""" ) + + +def test_check_csp_headers_non_xss_capable(http_resource_https): + headers_to_test = [ + ("Content-security-poliCY", "text/html"), + ("content-security-policy", "http://abc.com"), + ("content-security-policy", "https://abc.com"), + ("content-security-policy", "https://*.com"), + ("content-security-policy", "https://a.com; ...; media-src 'self'; media-src 10.10.10.10;"), + ("content-security-policy", "unsafe-inline-uri * strict-dynamic; test http: 127.0.0.1"), + ] + + results = [] + + # Iterate over headers and execute `run` function for each adding the content type header for xss capability check + for key, value in headers_to_test: + result = list( + run( + resource=http_resource_https, + additional_oois=[ + HTTPHeader(resource=http_resource_https.reference, key="Content-Type", value="text/plain"), + HTTPHeader(resource=http_resource_https.reference, key=key, value=value), + ], + config={}, + ) + ) + results.append(result) + + for result in results: + assert result == []