Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore missing csp if page is not xss capable #3126

Merged
merged 19 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions octopoes/bits/check_csp_header/bit.py
Original file line number Diff line number Diff line change
@@ -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",
)
52 changes: 39 additions & 13 deletions octopoes/bits/check_csp_header/check_csp_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.")
Expand Down Expand Up @@ -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,
)
Expand Down
17 changes: 16 additions & 1 deletion octopoes/bits/missing_headers/missing_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
67 changes: 55 additions & 12 deletions octopoes/tests/test_bit_csp_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 == []