Skip to content

Commit

Permalink
reorganize constants
Browse files Browse the repository at this point in the history
  • Loading branch information
juerkkil committed Oct 19, 2024
1 parent 357f87e commit 7716640
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 79 deletions.
69 changes: 69 additions & 0 deletions secheaders/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
from . import utils

# If no URL scheme defined, what to use by default
DEFAULT_URL_SCHEME = 'https'
DEFAULT_TIMEOUT = 10

# Let's try to imitate a legit browser to avoid being blocked / flagged as web crawler
REQUEST_HEADERS = {
'Accept': ('text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
'application/signed-exchange;v=b3;q=0.9'),
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-GB,en;q=0.9',
'Cache-Control': 'max-age=0',
'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
'Chrome/106.0.0.0 Safari/537.36'),
}

EVAL_WARN = 0
EVAL_OK = 1

# There are no universal rules for "safe" and "unsafe" CSP directives, but we apply some common sense here to
# catch some risky configurations
UNSAFE_CSP_RULES = {
"script-src": ["*", "'unsafe-eval'", "data:", "'unsafe-inline'"],
"frame-ancestors": ["*"],
"form-action": ["*"],
"object-src": ["*"],
}

# Configuring Permission-Policy is very case-specific and it's difficult to define a particular recommendation.
# We apply here a logic, that access to privacy-sensitive features and payments API should be restricted.
RESTRICTED_PERM_POLICY_FEATURES = ['camera', 'geolocation', 'microphone', 'payment']

# Headers that we try to sniff for version information
SERVER_VERSION_HEADERS = [
'x-powered-by',
'server',
'x-aspnet-version',
]

HEADER_STRUCTURED_LIST = [ # Response headers that define multiple values as comma-sparated list
'permissions-policy',
]

SECURITY_HEADERS_DICT = {
'x-frame-options': {
'recommended': True,
'eval_func': utils.eval_x_frame_options,
},
'strict-transport-security': {
'recommended': True,
'eval_func': utils.eval_sts,
},
'content-security-policy': {
'recommended': True,
'eval_func': utils.eval_csp,
},
'x-content-type-options': {
'recommended': True,
'eval_func': utils.eval_content_type_options,
},
'x-xss-protection': {
# X-XSS-Protection is deprecated; not supported anymore, and may be even dangerous in older browsers
'recommended': False,
'eval_func': utils.eval_x_xss_protection,
},
'referrer-policy': {
'recommended': True,
'eval_func': utils.eval_referrer_policy,
},
'permissions-policy': {
'recommended': True,
'eval_func': utils.eval_permissions_policy,
}
}
70 changes: 9 additions & 61 deletions secheaders/securityheaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,64 +7,12 @@
from urllib.parse import ParseResult, urlparse

from . import utils
from .constants import DEFAULT_TIMEOUT, DEFAULT_URL_SCHEME, EVAL_WARN
from .constants import DEFAULT_TIMEOUT, DEFAULT_URL_SCHEME, EVAL_WARN, REQUEST_HEADERS, HEADER_STRUCTURED_LIST, \
SECURITY_HEADERS_DICT, SERVER_VERSION_HEADERS
from .exceptions import SecurityHeadersException, InvalidTargetURL, UnableToConnect


class SecurityHeaders():
# Let's try to imitate a legit browser to avoid being blocked / flagged as web crawler
REQUEST_HEADERS = {
'Accept': ('text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
'application/signed-exchange;v=b3;q=0.9'),
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-GB,en;q=0.9',
'Cache-Control': 'max-age=0',
'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
'Chrome/106.0.0.0 Safari/537.36'),
}

SECURITY_HEADERS_DICT = {
'x-frame-options': {
'recommended': True,
'eval_func': utils.eval_x_frame_options,
},
'strict-transport-security': {
'recommended': True,
'eval_func': utils.eval_sts,
},
'content-security-policy': {
'recommended': True,
'eval_func': utils.eval_csp,
},
'x-content-type-options': {
'recommended': True,
'eval_func': utils.eval_content_type_options,
},
'x-xss-protection': {
# X-XSS-Protection is deprecated; not supported anymore, and may be even dangerous in older browsers
'recommended': False,
'eval_func': utils.eval_x_xss_protection,
},
'referrer-policy': {
'recommended': True,
'eval_func': utils.eval_referrer_policy,
},
'permissions-policy': {
'recommended': True,
'eval_func': utils.eval_permissions_policy,
}
}

SERVER_VERSION_HEADERS = [
'x-powered-by',
'server',
'x-aspnet-version',
]

HEADER_STRUCTURED_LIST = [ # Response headers that define multiple values as comma-sparated list
'permissions-policy',
]

def __init__(self, url, max_redirects=2, no_check_certificate=False):
parsed = urlparse(url)
if not parsed.scheme and not parsed.netloc:
Expand Down Expand Up @@ -106,7 +54,7 @@ def _follow_redirect_until_response(self, url, follow_redirects=5):
raise InvalidTargetURL("Unsupported protocol scheme")

try:
conn.request('GET', temp_url.path, headers=self.REQUEST_HEADERS)
conn.request('GET', temp_url.path, headers=REQUEST_HEADERS)
res = conn.getresponse()
except (socket.gaierror, socket.timeout, ConnectionRefusedError) as e:
raise UnableToConnect("Connection failed {}".format(temp_url.netloc)) from e
Expand Down Expand Up @@ -156,15 +104,15 @@ def fetch_headers(self):

conn = self.open_connection(self.target_url)
try:
conn.request('GET', self.target_url.path, headers=self.REQUEST_HEADERS)
conn.request('GET', self.target_url.path, headers=REQUEST_HEADERS)
res = conn.getresponse()
except (socket.gaierror, socket.timeout, ConnectionRefusedError, ssl.SSLError) as e:
raise UnableToConnect("Connection failed {}".format(self.target_url.hostname)) from e

headers = res.getheaders()
for h in headers:
key = h[0].lower()
if key in self.HEADER_STRUCTURED_LIST and key in self.headers:
if key in HEADER_STRUCTURED_LIST and key in self.headers:
# Scenario described in RFC 2616 section 4.2
self.headers[key] += ', {}'.format(h[1])
else:
Expand All @@ -178,9 +126,9 @@ def check_headers(self):
raise SecurityHeadersException("Headers not fetched successfully")

""" Loop through headers and evaluate the risk """
for header in self.SECURITY_HEADERS_DICT:
for header in SECURITY_HEADERS_DICT:
if header in self.headers:
eval_func = self.SECURITY_HEADERS_DICT[header].get('eval_func')
eval_func = SECURITY_HEADERS_DICT[header].get('eval_func')
if not eval_func:
raise SecurityHeadersException("No evaluation function found for header: {}".format(header))
res, notes = eval_func(self.headers[header])
Expand All @@ -192,10 +140,10 @@ def check_headers(self):
}

else:
warn = self.SECURITY_HEADERS_DICT[header].get('recommended')
warn = SECURITY_HEADERS_DICT[header].get('recommended')
retval[header] = {'defined': False, 'warn': warn, 'contents': None, 'notes': []}

for header in self.SERVER_VERSION_HEADERS:
for header in SERVER_VERSION_HEADERS:
if header in self.headers:
res, notes = utils.eval_version_info(self.headers[header])
retval[header] = {
Expand Down
22 changes: 4 additions & 18 deletions secheaders/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import re
from typing import Tuple


from .constants import EVAL_WARN, EVAL_OK
from .constants import EVAL_WARN, EVAL_OK, UNSAFE_CSP_RULES, RESTRICTED_PERM_POLICY_FEATURES


def eval_x_frame_options(contents: str) -> Tuple[int, list]:
Expand Down Expand Up @@ -38,25 +37,16 @@ def eval_sts(contents: str) -> Tuple[int, list]:


def eval_csp(contents: str) -> Tuple[int, list]:
UNSAFE_RULES = {
"script-src": ["*", "'unsafe-eval'", "data:", "'unsafe-inline'"],
"frame-ancestors": ["*"],
"form-action": ["*"],
"object-src": ["*"],
}

# There are no universal rules for "safe" and "unsafe" CSP directives, but we apply some common sense here to
# catch some obvious lacks or poor configuration
csp_unsafe = False
csp_notes = []

csp_parsed = csp_parser(contents)

for rule in UNSAFE_RULES:
for rule in UNSAFE_CSP_RULES:
if rule not in csp_parsed:
if '-src' in rule and 'default-src' in csp_parsed:
# fallback to default-src
for unsafe_src in UNSAFE_RULES[rule]:
for unsafe_src in UNSAFE_CSP_RULES[rule]:
if unsafe_src in csp_parsed['default-src']:
csp_unsafe = True
csp_notes.append("Directive {} not defined, and default-src contains unsafe source {}".format(
Expand All @@ -65,7 +55,7 @@ def eval_csp(contents: str) -> Tuple[int, list]:
csp_notes.append("No directive {} nor default-src defined in the Content Security Policy".format(rule))
csp_unsafe = True
else:
for unsafe_src in UNSAFE_RULES[rule]:
for unsafe_src in UNSAFE_CSP_RULES[rule]:
if unsafe_src in csp_parsed[rule]:
csp_notes.append("Unsafe source {} in directive {}".format(unsafe_src, rule))
csp_unsafe = True
Expand All @@ -85,13 +75,9 @@ def eval_version_info(contents: str) -> Tuple[int, list]:


def eval_permissions_policy(contents: str) -> Tuple[int, list]:
# Configuring Permission-Policy is very case-specific and it's difficult to define a particular recommendation.
# We apply here a logic, that access to privacy-sensitive features and payments API should be restricted.

pp_parsed = permissions_policy_parser(contents)
notes = []
pp_unsafe = False
RESTRICTED_PERM_POLICY_FEATURES = ['camera', 'geolocation', 'microphone', 'payment']

for feature in RESTRICTED_PERM_POLICY_FEATURES:
feat_policy = pp_parsed.get(feature)
Expand Down

0 comments on commit 7716640

Please sign in to comment.