From 188fc32b173bcb30aeff50546fa360ad6ae558eb Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Mon, 12 Jun 2023 12:31:26 +0200 Subject: [PATCH] removed expect-ct it's deprecated (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT) --- README.md | 6 +-- securityheaders/checkers/__init__.py | 1 - securityheaders/checkers/expectct/__init__.py | 5 -- securityheaders/checkers/expectct/checker.py | 9 ---- .../checkers/expectct/httpreporturi.py | 17 ------- .../checkers/expectct/notenforce.py | 22 -------- .../checkers/expectct/test_httpreporturi.py | 29 ----------- .../checkers/expectct/test_notenforce.py | 51 ------------------- securityheaders/models/__init__.py | 5 +- securityheaders/models/expectct/__init__.py | 4 -- securityheaders/models/expectct/expectct.py | 34 ------------- .../models/expectct/expectctdirective.py | 29 ----------- securityheaders/securityheader.py | 26 +++++----- 13 files changed, 19 insertions(+), 219 deletions(-) delete mode 100644 securityheaders/checkers/expectct/__init__.py delete mode 100644 securityheaders/checkers/expectct/checker.py delete mode 100644 securityheaders/checkers/expectct/httpreporturi.py delete mode 100644 securityheaders/checkers/expectct/notenforce.py delete mode 100644 securityheaders/checkers/expectct/test_httpreporturi.py delete mode 100644 securityheaders/checkers/expectct/test_notenforce.py delete mode 100644 securityheaders/models/expectct/__init__.py delete mode 100644 securityheaders/models/expectct/expectct.py delete mode 100644 securityheaders/models/expectct/expectctdirective.py diff --git a/README.md b/README.md index 605f9ac..d41438c 100755 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ Install it as follows. 1. Go to Extender, Extensions, and click on Add Extension. Select python and load the burpecheaders.py file. ![Load the burpsecheaders.py file](./pics/burp1.png) 2. Once BurpSuite loads the plugin successfully, visit a website and observe that the plugin reports issues under the scanner tab. - ![Scanner shows issues of the plugin](./pics/burp2.png) + ![Scanner shows issues of the plugin](./pics/burp2.png) Observe that the plugin highlights the offending header/directives/keywords in the response headers. ![BurpSuite highlights the insecure headers](./pics/burp3.png) @@ -405,7 +405,7 @@ The HTTP Strict Transport Security (HSTS) header ensures that all communication The header has the following directives: -- **max-age**: specifies the number of seconds the browser regards the host as a known HSTS Host. +- **max-age**: specifies the number of seconds the browser regards the host as a known HSTS Host. - **includeSubdomains**: this optional directive indicates that the HSTS Policy applies to this HSTS Host as well as any subdomains of the host's domain name. - **preload**: the `preload` directive indicates that the domain can be preloaded in the browser as a known HSTS host. @@ -681,7 +681,7 @@ The tool also identifies the following syntactical errors (`SyntaxChecker`) for ``` - **Missing Directive** (`MissingDirectiveChecker`): using a header without a required directive is an issue. The tool will thus mark the following as an error as max-age is missing. ```http - Expect-CT: enforce + Strict-Transport-Security: includeSubDomains ``` - **Empty Directives** (`EmptyDirectiveChecker`): using a directive without a required value is an issue. The tool will thus mark the following as an error. ```http diff --git a/securityheaders/checkers/__init__.py b/securityheaders/checkers/__init__.py index c330139..a9e4de7 100644 --- a/securityheaders/checkers/__init__.py +++ b/securityheaders/checkers/__init__.py @@ -34,7 +34,6 @@ from .xpoweredby import * from .xxssprotection import * from .other import * -from .expectct import * from .xpcdp import * from .setcookie import * diff --git a/securityheaders/checkers/expectct/__init__.py b/securityheaders/checkers/expectct/__init__.py deleted file mode 100644 index 42f5916..0000000 --- a/securityheaders/checkers/expectct/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .checker import ExpectCTChecker -from .httpreporturi import ExpectCTHTTPReportURIChecker -from .notenforce import ExpectCTNotEnforcedChecker - -__all__ = ['ExpectCTChecker','ExpectCTHTTPReportURIChecker','ExpectCTNotEnforcedChecker'] diff --git a/securityheaders/checkers/expectct/checker.py b/securityheaders/checkers/expectct/checker.py deleted file mode 100644 index 0608753..0000000 --- a/securityheaders/checkers/expectct/checker.py +++ /dev/null @@ -1,9 +0,0 @@ -from securityheaders.models.expectct import ExpectCT -from securityheaders.checkers import Checker - -class ExpectCTChecker(Checker): - def __init__(self): - pass - - def getexpectct(self, headers): - return self.extractheader(headers, ExpectCT) diff --git a/securityheaders/checkers/expectct/httpreporturi.py b/securityheaders/checkers/expectct/httpreporturi.py deleted file mode 100644 index d996ac4..0000000 --- a/securityheaders/checkers/expectct/httpreporturi.py +++ /dev/null @@ -1,17 +0,0 @@ -from securityheaders.checkers import Finding, FindingType, FindingSeverity - -from .checker import ExpectCTChecker - -class ExpectCTHTTPReportURIChecker(ExpectCTChecker): - - def check(self, headers, opt_options=dict()): - findings = [] - expectct = self.getexpectct(headers) - - if not expectct: - return findings - - findings = [] - if expectct.reporturi() and expectct.reporturi().startswith('http://'): - findings.append(Finding(expectct.headerkey,FindingType.SRC_HTTP,expectct.headerkey + 'communicates its reports via an insecure channel.', FindingSeverity.LOW, expectct.reporturi())) - return findings diff --git a/securityheaders/checkers/expectct/notenforce.py b/securityheaders/checkers/expectct/notenforce.py deleted file mode 100644 index 8483bf7..0000000 --- a/securityheaders/checkers/expectct/notenforce.py +++ /dev/null @@ -1,22 +0,0 @@ -from securityheaders.models import ExpectCT -from securityheaders.checkers import Finding, FindingType, FindingSeverity - -from .checker import ExpectCTChecker - -class ExpectCTNotEnforcedChecker(ExpectCTChecker): - - def check(self, headers, opt_options=dict()): - findings = [] - expectct = self.getexpectct(headers) - - if not expectct: - return findings - - findings = [] - if not expectct.enforce(): - findings.append(Finding(expectct.headerkey,FindingType.NOT_ENFORCED,expectct.headerkey + 'is not enforced as ' + ExpectCT.directive.ENFORCE.value + ' is not set.', FindingSeverity.LOW, ExpectCT.directive.ENFORCE,None)) - if expectct.maxage() == 0: - findings.append(Finding(expectct.headerkey,FindingType.NOT_ENFORCED,expectct.headerkey + 'is not enforced as ' + ExpectCT.directive.MAX_AGE.value + ' is set to 0', FindingSeverity.LOW, ExpectCT.directive.MAX_AGE,'0')) - elif expectct.maxage() and expectct.maxage() < 3000: - findings.append(Finding(expectct.headerkey,FindingType.NOT_ENFORCED,expectct.headerkey + 'is only enforced for a very short amount of time as ' + ExpectCT.directive.MAX_AGE.value + ' is set to ' + str(expectct.maxage()), FindingSeverity.LOW, ExpectCT.directive.MAX_AGE,str(expectct.maxage()))) - return findings diff --git a/securityheaders/checkers/expectct/test_httpreporturi.py b/securityheaders/checkers/expectct/test_httpreporturi.py deleted file mode 100644 index 6138fb4..0000000 --- a/securityheaders/checkers/expectct/test_httpreporturi.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest - -from securityheaders.checkers.expectct import ExpectCTHTTPReportURIChecker - -class ExpectCTHTTPReportURICheckerTest(unittest.TestCase): - def setUp(self): - self.x = ExpectCTHTTPReportURIChecker() - - def test_check(self): - xempty = dict() - self.assertEqual(self.x.check(xempty), []) - - def test_checkNone(self): - xnone = None - self.assertEqual(self.x.check(xnone), []) - - def test_Good(self): - xhas = dict() - xhas['expect-ct'] = 'max-age=10, enforce, report-uri=https://google.com/report' - self.assertEqual(self.x.check(xhas), []) - - def test_Bad(self): - xhasbad = dict() - xhasbad['expect-ct'] = 'max-age=10, enforce, report-uri=http://google.com/report' - result = self.x.check(xhasbad) - self.assertIsNotNone(result) - self.assertEqual(len(result), 1) -if __name__ == '__main__': - unittest.main() diff --git a/securityheaders/checkers/expectct/test_notenforce.py b/securityheaders/checkers/expectct/test_notenforce.py deleted file mode 100644 index 768bc73..0000000 --- a/securityheaders/checkers/expectct/test_notenforce.py +++ /dev/null @@ -1,51 +0,0 @@ -import unittest - -from securityheaders.checkers.expectct import ExpectCTNotEnforcedChecker - -class ExpectCTNotEnforcedCheckerTest(unittest.TestCase): - def setUp(self): - self.x = ExpectCTNotEnforcedChecker() - - def test_check(self): - xempty = dict() - self.assertEqual(self.x.check(xempty), []) - - def test_checkNone(self): - xnone = None - self.assertEqual(self.x.check(xnone), []) - - def test_Good(self): - xhas = dict() - xhas['expect-ct'] = 'max-age=500000, enforce, report-uri=https://google.com' - self.assertEqual(self.x.check(xhas), []) - - def test_Bad(self): - xhasbad = dict() - xhasbad['expect-ct'] = 'max-age=20000, report-uri=https://google.com' - result = self.x.check(xhasbad) - self.assertIsNotNone(result) - self.assertEqual(len(result), 1) - - def test_Bad2(self): - xhasbad = dict() - xhasbad['expect-ct'] = 'max-age=0, enforce, report-uri=https://google.com' - result = self.x.check(xhasbad) - self.assertIsNotNone(result) - self.assertEqual(len(result), 1) - - def test_Bad3(self): - xhasbad = dict() - xhasbad['expect-ct'] = 'max-age=0, report-uri=https://google.com' - result = self.x.check(xhasbad) - self.assertIsNotNone(result) - self.assertEqual(len(result), 2) - - def test_Bad4(self): - xhasbad = dict() - xhasbad['expect-ct'] = 'max-age=10, enforce, report-uri=https://google.com' - result = self.x.check(xhasbad) - self.assertIsNotNone(result) - self.assertEqual(len(result), 1) - -if __name__ == '__main__': - unittest.main() diff --git a/securityheaders/models/__init__.py b/securityheaders/models/__init__.py index 0fbf65b..d91028b 100644 --- a/securityheaders/models/__init__.py +++ b/securityheaders/models/__init__.py @@ -27,14 +27,15 @@ from .xwebkitcsp import * from .xcsp import * from .xdownloadoptions import * -from .expectct import * from .xaspnetversion import * from .xaspnetmvcversion import * from .hpkp import * from .xpcdp import * from .setcookie import * -__all__ = ['annotations','csp','cors','clearsitedata','hsts','xcontenttypeoptions','xframeoptions','xxssprotection','featurepolicy','referrerpolicy','server','xpoweredby', 'expectct','xcsp','xwebkitcsp','xpcdp','xaspnetversion','xaspnetmvcversion','hpkp','xdownloadoptions'] +__all__ = ['annotations', 'csp', 'cors', 'clearsitedata', 'hsts', 'xcontenttypeoptions', 'xframeoptions', + 'xxssprotection', 'featurepolicy', 'referrerpolicy', 'server', 'xpoweredby', 'xcsp', 'xwebkitcsp', 'xpcdp', + 'xaspnetversion', 'xaspnetmvcversion', 'hpkp', 'xdownloadoptions'] clazzes = list(Util.inheritors(Header)) clazzes.extend(Util.inheritors(Directive)) clazzes.extend(Util.inheritors(Keyword)) diff --git a/securityheaders/models/expectct/__init__.py b/securityheaders/models/expectct/__init__.py deleted file mode 100644 index 4456b7b..0000000 --- a/securityheaders/models/expectct/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .expectctdirective import ExpectCTDirective -from .expectct import ExpectCT - -__all__ = ['ExpectCTDirective','ExpectCT'] diff --git a/securityheaders/models/expectct/expectct.py b/securityheaders/models/expectct/expectct.py deleted file mode 100644 index f91d351..0000000 --- a/securityheaders/models/expectct/expectct.py +++ /dev/null @@ -1,34 +0,0 @@ -from securityheaders.models import SecurityHeader -from securityheaders.models.expectct import ExpectCTDirective -from securityheaders.models.annotations import * - -@requiredheader -@description('Expect-CT allows a site to determine if they are ready to enforce their CT policy.') -@headername('expect-ct') -@headerref('https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-07') -class ExpectCT(SecurityHeader): - directive = ExpectCTDirective - - def __init__(self, unparsedstring): - SecurityHeader.__init__(self, unparsedstring, ExpectCTDirective) - - def enforce(self): - try: - result = ExpectCTDirective.ENFORCE in self.parsedstring - return result - except: - return False - - def reporturi(self): - try: - return self.parsedstring[ExpectCTDirective.REPORT_URI][0] - except IndexError: - return "" #there is a key, but it is empty - except KeyError: - return None #there is no key - - def maxage(self): - try: - return int(self.parsedstring[ExpectCTDirective.MAX_AGE][0]) - except Exception: - return None diff --git a/securityheaders/models/expectct/expectctdirective.py b/securityheaders/models/expectct/expectctdirective.py deleted file mode 100644 index c27a61e..0000000 --- a/securityheaders/models/expectct/expectctdirective.py +++ /dev/null @@ -1,29 +0,0 @@ -from securityheaders.models import Directive -from securityheaders.models.annotations import requireddirectives, requireddirectivevalues - -@requireddirectives('max-age') -@requireddirectivevalues('max-age','report-uri') -class ExpectCTDirective(Directive): - REPORT_URI = 'report-uri' - ENFORCE = 'enforce' - MAX_AGE = 'max-age' - - - @classmethod - def isDirective(cls, directive): - if isinstance(directive, ExpectCTDirective): - return True - return any(directive.lower() == item.value.lower() for item in list(cls)) - - - @classmethod - def directiveseperator(cls): - return ',' - - @classmethod - def valueseperator(cls): - return '=' - - @classmethod - def directivevalueseperator(cls): - return '=' diff --git a/securityheaders/securityheader.py b/securityheaders/securityheader.py index 4041c64..23376ce 100644 --- a/securityheaders/securityheader.py +++ b/securityheaders/securityheader.py @@ -9,7 +9,7 @@ import ssl import random from functools import partial -from anytree import ContStyle, RenderTree +from anytree import ContStyle, RenderTree try: from multiprocessing import Pool, freeze_support @@ -44,14 +44,14 @@ def _unpickle_method(func_name, obj, cls): break return func.__get__(obj, cls) -try: +try: import copy_reg except ModuleNotFoundError: import copyreg as copy_reg import types copy_reg.pickle(types.MethodType, _pickle_method, _unpickle_method) - + class SecurityHeaders(object): def __init__(self): @@ -75,7 +75,7 @@ def get_options(self): def get_all_checker_names(self): return CheckerFactory().getnames() - + def get_all_header_names(self): return sorted(ModelFactory().getheadernames()) @@ -137,7 +137,7 @@ def check_headers_with_map(self, headermap, options=None): options['checks'] = [] if not 'unwanted' in options.keys(): options['unwanted'] = [] - checks = CheckerFactory().getactualcheckers(options['checks']) + checks = CheckerFactory().getactualcheckers(options['checks']) unwanted = CheckerFactory().getactualcheckers(options['unwanted']) options['checks'] = [e for e in checks if e not in unwanted] options['unwanted'] = None @@ -149,7 +149,7 @@ def check_headers_with_map(self, headermap, options=None): for check in options[checker].keys(): if leaf not in options.keys(): options[leaf]=dict() - options[leaf][check] = options[checker][check] + options[leaf][check] = options[checker][check] return HeaderEvaluator().evaluate(headermap,options) def check_headers_parallel(self, urls, options=None, callback=None): @@ -164,7 +164,7 @@ def check_headers_parallel(self, urls, options=None, callback=None): result = pool.apply_async(self.check_headers, args=(url, options.get('redirects'), options), callback=callback) results.append(result) pool.close() - pool.join() + pool.join() return results else: raise Exception('no parallelism supported') @@ -175,7 +175,7 @@ def check_headers(self, url, follow_redirects = 3, options=None): Args: url (str): Target URL in format: scheme://hostname/path/to/file - follow_redirects (Optional[str]): How deep we follow the redirects, + follow_redirects (Optional[str]): How deep we follow the redirects, value 0 disables redirects. """ if not options: @@ -193,7 +193,7 @@ def check_headers(self, url, follow_redirects = 3, options=None): else: url = 'https://' + url.strip() - parsed = urlparse(url) + parsed = urlparse(url) hostname = parsed.netloc if not hostname: return [] @@ -222,7 +222,7 @@ def check_headers(self, url, follow_redirects = 3, options=None): else: """ Unknown protocol scheme """ return {} - + conn.request('GET', path, None, headers) res = conn.getresponse() headers = res.getheaders() @@ -231,14 +231,14 @@ def check_headers(self, url, follow_redirects = 3, options=None): if (res.status >= 300 and res.status < 400 and follow_redirects > 0): for header in headers: if (header[0] == 'location'): - return self.check_headers((urlid, header[1]), follow_redirects - 1, options) - + return self.check_headers((urlid, header[1]), follow_redirects - 1, options) + """ Loop through headers and evaluate the risk """ result = self.check_headers_with_map(headers, options) for finding in result: finding.url = url finding.urlid = urlid - + return result except Exception as e: return [Finding(None, FindingType.ERROR, str(e), FindingSeverity.ERROR, None, None,url , urlid)]