├── .gitignore ├── LICENSE ├── README.md ├── readme_imgs ├── first.png └── second.png ├── tests └── test_headers.py ├── vkb.md └── yasha.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.xml 2 | cache_report.htm 3 | csp_report.htm 4 | reporting.htm 5 | TODO.md 6 | yasha_log.* 7 | burp_history 8 | __pycache__* 9 | .vscode* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, LRQA Nettitude 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YASHA - Yet Another Security Header Analyser 2 | 3 | **Note that this is still in development. Please use the logs option and compare output to ensure accuracy.** 4 | 5 | > [!Warning] 6 | > Don't use this with untrusted data. See https://docs.python.org/3/library/xml.html#xml-vulnerabilities. 7 | 8 | # What file does YASHA take? 9 | Yasha uses Burp Suite history. Ideally, first narrow traffic to in-scope items. 10 | 11 | ![Narrowing to in-scope](/readme_imgs/first.png) 12 | 13 | Then save items to a file. 14 | 15 | ![Save items from history](/readme_imgs/second.png) 16 | 17 | # Usage tips 18 | - YASHA writes a copy-and-paste-ready output into a file that should work for copy-and-pasting for a number of editors. 19 | - If something doesn't seem right, the detailed output for URLs and headers are available using `--log`. Use your browser's builtin JSON viewing and filtering or a tool like `jq` to analyse it. 20 | - Yasha needs you to identify if the right (usually sensitive) endpoints have caching headers. It helps by showing you a list so that you can make a judgement: if there are a few URLs, it will do so in the terminal, and else it will output a HTML file. 21 | - It also needs you to look at the CSP headers yourself. It will help by outputting a HTML file with links that can open that CSP header in Google's CSP Evaluator. 22 | 23 | # If using YASHA as a library 24 | YASHA is quite talkative: it will need to mention what it is analysing, and ask the user for input. It will also write `cache_report.htm` and (often) `csp_report.htm`. 25 | 26 | The only function you need is `yasha()`. It requires the path to the Burp output file, and optionally takes a `log` Boolean argument that will output the results. 27 | The function will return a `parcel`: a dictionary that contains `output` (a dictionary of the results), `report` which is a HTML page in string form with the VKB for adding to the editor of your choice, and optionally `log` which is a JSON of the original results of the analysis before anything was done to them. 28 | 29 | Barebones example: 30 | 31 | ```python 32 | from yasha import yasha 33 | 34 | filename = input('What is the filename? > ') 35 | try: 36 | for_editor = yasha(filename)['report'] 37 | except FileNotFoundError: 38 | print("Couldn't find the file.") 39 | 40 | print("Here is the output.\n\n") 41 | print(for_editor) 42 | ``` 43 | 44 | # Editing the vkb.md File 45 | Currently the VKB file has been populated with information from the MDN Web Docs, which has its own copyright and attribution requirements. 46 | 47 | If you would like to replace it with your own custom writeup, ideally you should leave the headings alone and simply edit the bodies under them. Use backticks for code. 48 | 49 | The references in further reading are matched up to the first word or first three words, so editing the links should be fine. If you do want to edit the whole thing, edit `rec_translation` in the `report()` function. 50 | -------------------------------------------------------------------------------- /readme_imgs/first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nettitude/yasha/9a12eba1909b2a9e0b0c709e2289e12c82fb8d2d/readme_imgs/first.png -------------------------------------------------------------------------------- /readme_imgs/second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nettitude/yasha/9a12eba1909b2a9e0b0c709e2289e12c82fb8d2d/readme_imgs/second.png -------------------------------------------------------------------------------- /tests/test_headers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | from typing import List 4 | 5 | import yasha 6 | 7 | def capsicum(s: str, n: int = 10) -> List[str]: 8 | if not s: 9 | return [""] 10 | 11 | indices = list(range(len(s))) 12 | results = [] 13 | 14 | for _ in range(n): 15 | to_capitalise = random.sample(indices, random.choice(indices)) 16 | results.append(''.join([x.upper() if i in to_capitalise else x for i, x in enumerate(s)])) 17 | 18 | return results 19 | 20 | class TestHeaders(unittest.TestCase): 21 | def test_hsts_checks(self): 22 | checks_and_results = { 23 | "Strict-Transport-Security: max-age=31536000; includeSubDomains": ["pass"], 24 | "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload": ["pass"], 25 | "" : ['fail', 'No HSTS header found'], 26 | "Strict-Transport-Security: max-age=31535999; includeSubDomains": ['fail', 'max-age set to 31535999'], 27 | "Strict-Transport-Security: includeSubDomains": ['fail', 'HSTS does not contain max age'], 28 | } 29 | for check in checks_and_results.keys(): 30 | result = yasha.hsts_check([check]) 31 | self.assertEqual(result, checks_and_results[check]) 32 | 33 | for case in capsicum(check): 34 | with self.subTest(case=case): 35 | self.assertEqual(result, yasha.hsts_check([case])) 36 | 37 | def test_xframeoptions_check(self): 38 | checks_and_results = { 39 | "X-Frame-Options: DENY": ['pass'], 40 | "X-Frame-Options: SAMEORIGIN": ['pass'], 41 | "X-Frame-Options: QUACKQUACK": ['fail', 'X-Frame-Options value is QUACKQUACK'], 42 | } 43 | for check in checks_and_results.keys(): 44 | result = yasha.xframeoptions_check([check]) 45 | self.assertEqual(result, checks_and_results[check]) 46 | 47 | for case in capsicum(check): 48 | with self.subTest(case=case): 49 | self.assertEqual(list(map(lambda x: x.lower(), result)), list(map(lambda x: x.lower(), yasha.xframeoptions_check([case])))) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() -------------------------------------------------------------------------------- /vkb.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | Some HTTP security headers are either misconfigured or missing. 3 | 4 | # Background 5 | Note that the content here is from the MDN Web Docs: 6 | Attributions and copyright licensing by Mozilla Contributors is licensed under CC-BY-SA 2.5. 7 | 8 | Websites contain several different types of information. Some of it is non-sensitive, for example the copy shown on the public pages. Some of it is sensitive, for example customer usernames, passwords, and banking information, or internal algorithms and private product information. 9 | 10 | Modern browsers already have several features to protect users' security on the web, but developers also need to employ best practices and code carefully to ensure that their websites are secure. Even simple bugs in your code can result in vulnerabilities that bad people can exploit to steal data and gain control over services for which they don't have authorization. 11 | 12 | The following security headers were identified as being either missing or misconfigured. 13 | 14 | ## HTTP Strict Transport Security (HSTS) 15 | The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) informs browsers that the site should only be accessed using HTTPS, and that any future attempts to access it using HTTP should automatically be converted to HTTPS. This is more secure than simply configuring a HTTP to HTTPS (301) redirect on your server, where the initial HTTP connection is still vulnerable to a man-in-the-middle attack. 16 | 17 | The following HTTP response header can be used to enforce HTTPS for a max-age of 1 year. This blocks access to pages or subdomains that can only be served over HTTP: 18 | 19 | `Strict-Transport-Security: max-age=31536000; includeSubDomains` 20 | 21 | Although a max-age of 1 year is acceptable for a domain, two years is the recommended value as explained on https://hstspreload.org. 22 | 23 | In the following example, max-age is set to 2 years, and is suffixed with preload, which is necessary for inclusion in all major web browsers' HSTS preload lists, like Chromium, Edge, and Firefox. 24 | 25 | `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` 26 | 27 | ## Content Security Policy (CSP) 28 | 29 | Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft, to site defacement, to malware distribution. 30 | 31 | CSP is designed to be fully backward compatible (except CSP version 2 where there are some explicitly-mentioned inconsistencies in backward compatibility). Browsers that don't support it still work with servers that implement it, and vice versa: browsers that don't support CSP ignore it, functioning as usual, defaulting to the standard same-origin policy for web content. If the site doesn't offer the CSP header, browsers likewise use the standard same-origin policy. 32 | 33 | To enable CSP, you need to configure your web server to return the Content-Security-Policy HTTP header. (Sometimes you may see mentions of the `X-Content-Security-Policy` header, but that's an older version and you don't need to specify it anymore.) 34 | 35 | You can use the Content-Security-Policy HTTP header to specify your policy, like this: 36 | 37 | `Content-Security-Policy: policy` 38 | 39 | The policy is a string containing the policy directives describing your Content Security Policy. 40 | 41 | ## Clickjacking 42 | 43 | The X-Frame-Options HTTP response header can be used to indicate whether a browser should be allowed to render a page in a `<frame\>`, `<iframe>`, `<embed>` or `<object>`. Sites can use this to avoid click-jacking attacks, by ensuring that their content is not embedded into other sites. 44 | The following HTTP response header can be used to prevent the application from being framed in undesirable locations: 45 | 46 | The added security is provided only if the user accessing the document is using a browser that supports X-Frame-Options. Note that the Content-Security-Policy HTTP header has a `frame-ancestors` directive which obsoletes this header for supporting browsers. 47 | 48 | There are two possible directives for X-Frame-Options: 49 | 50 | `X-Frame-Options: DENY` 51 | 52 | `X-Frame-Options: SAMEORIGIN` 53 | 54 | If you specify `DENY`, not only will the browser attempt to load the page in a frame fail when loaded from other sites, attempts to do so will fail when loaded from the same site. On the other hand, if you specify `SAMEORIGIN`, you can still use the page in a frame as long as the site including it in a frame is the same as the one serving the page. 55 | 56 | This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Instead of this header, use the frame-ancestors directive in a Content-Security-Policy header. 57 | 58 | ## Content Sniffing 59 | 60 | The `X-Content-Type-Options` response HTTP header is a marker used by the server to indicate that the MIME types advertised in the `Content-Type` headers should be followed and not be changed. The header allows you to avoid MIME type sniffing by saying that the MIME types are deliberately configured. 61 | 62 | This header was introduced by Microsoft in IE 8 as a way for webmasters to block content sniffing that was happening and could transform non-executable MIME types into executable MIME types. Since then, other browsers have introduced it, even if their MIME sniffing algorithms were less aggressive. 63 | 64 | Starting with Firefox 72, top-level documents also avoid MIME sniffing (if `Content-type` is provided). This can cause HTML web pages to be downloaded instead of being rendered when they are served with a MIME type other than text/html. Make sure to set both headers correctly. 65 | 66 | Site security testers usually expect this header to be set. 67 | 68 | `X-Content-Type-Options: nosniff` 69 | 70 | ## Cacheable HTTPS Response 71 | 72 | The `Cache-Control` HTTP header field holds directives (instructions) — in both requests and responses — that control caching in browsers and shared caches (e.g. Proxies, CDNs). 73 | 74 | If you don't want a response stored in caches, use the no-store directive. 75 | 76 | `Cache-Control: no-store` 77 | 78 | ## Referrer Policy 79 | 80 | The `Referrer-Policy` HTTP header controls how much referrer information (sent with the `Referer` header) should be included with requests. 81 | 82 | ## Permissions Policy 83 | 84 | The HTTP `Permissions-Policy` header provides a mechanism to allow and deny the use of browser features in a document or within any `<iframe>` elements in the document. 85 | 86 | ## X-XSS-Protection 87 | 88 | The HTTP `X-XSS-Protection` response header is a feature of Internet Explorer, Chrome and Safari that stops pages from loading when they detect reflected cross-site scripting (XSS) attacks. These protections are largely unnecessary in modern browsers when sites implement a strong `Content-Security-Policy` that disables the use of inline JavaScript (`'unsafe-inline'`). 89 | 90 | Warning: Even though this feature can protect users of older web browsers that don't yet support CSP, in some cases, XSS protection can create XSS vulnerabilities in otherwise safe websites. See the section below for more information. 91 | 92 | # Recommendations 93 | 94 | Employ best practices and code carefully to ensure web application security 95 | 96 | # Further Reading 97 | - OWASP HTTP Security Response Headers Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html 98 | - Security on the web - https://developer.mozilla.org/en-US/docs/Web/Security 99 | - Strict-Transport-Security - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 100 | - HSTS Preload List - https://hstspreload.org/ 101 | - Content Security Policy (CSP) - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 102 | - X-Frame-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 103 | - X-Content-Type-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 104 | - Cache-Control - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control 105 | - Referrer-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 106 | - Permissions-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy 107 | - X-XSS-Protection - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection -------------------------------------------------------------------------------- /yasha.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import argparse 3 | import base64 as b64 4 | import re 5 | import json 6 | from pathlib import Path 7 | from urllib.parse import urlparse 8 | from typing import List 9 | import random 10 | 11 | # Warning: don't use this with untrusted data. See https://docs.python.org/3/library/xml.html#xml-vulnerabilities. 12 | 13 | def head_filter(req, filter): 14 | return [x for x in req if (x.lower()).startswith(filter.lower())] 15 | 16 | def hsts_check(req: List[str]) -> List[str]: 17 | hst_lines = head_filter(req, 'Strict-Transport-Security') 18 | if hst_lines: 19 | # Remove Strict-Transport-Security from beginning 20 | hst_lines[0] = ''.join(hst_lines[0].split(':')[1:]) 21 | 22 | good_max_age: bool = False 23 | for line in hst_lines: 24 | values = list(map(lambda f: f.strip(), line.split(';'))) 25 | for value in values: 26 | value = value.strip() 27 | if value.lower().startswith('max-age'): 28 | age = int(value.split('=')[1]) 29 | # max-age has to be set to this value. See https://hstspreload.org/ 30 | if age < 31536000: 31 | return ['fail', f'max-age set to {age}'] 32 | else: 33 | good_max_age: bool = True 34 | if good_max_age: 35 | return ['pass'] 36 | else: 37 | return ['fail', 'HSTS does not contain max age'] 38 | else: 39 | return ['fail', 'No HSTS header found'] 40 | 41 | def xframeoptions_check(req: List[str]) -> List[str]: 42 | # This is obsoleted for supporting browsers, but not everywhere yet. 43 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 44 | xfo_lines = head_filter(req, 'X-Frame-Options') 45 | if xfo_lines: 46 | return ['fail', 'Deprecated X-Frame-Options headers found'] 47 | else: 48 | return ['pass', 'No X-Frame-Options header found; check CSP'] 49 | 50 | def xcontenttypeoptions_check(req): 51 | xcto_lines = head_filter(req, 'X-Content-Type-Options') 52 | if xcto_lines: 53 | for line in xcto_lines: 54 | value = (line.split(':')[1]).strip() 55 | if 'nosniff' not in value: 56 | return ['fail', f'X-Content-Type-Options set to {value}'] 57 | return ['pass'] 58 | else: 59 | return ['fail', 'No X-Content-Type-Options header found'] 60 | 61 | def xxssprotection_check(req): 62 | xxp_lines = head_filter(req, 'X-XSS-Protection') 63 | if xxp_lines: 64 | # You oughtn't have it unless your client has really outdated browsers. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection 65 | return ['fail', 'Outdated X-XSS-Protection headers found'] 66 | else: 67 | return ['pass', 'No X-XSS-Protection headers found'] 68 | 69 | def permissionspolicy_check(req): 70 | pp_lines = head_filter(req, 'Permissions-Policy') 71 | if pp_lines: 72 | pp_contrast = set() 73 | pp_contrast = {'accelerometer=()', 'ambient-light-sensor=()', 'autoplay=()',\ 74 | 'battery=()', 'camera=()', 'cross-origin-isolated=()',\ 75 | 'display-capture=()', 'document-domain=()', 'encrypted-media=()',\ 76 | 'execution-while-not-rendered=()',\ 77 | 'execution-while-out-of-viewport=()', 'fullscreen=()',\ 78 | 'geolocation=()', 'gyroscope=()', 'keyboard-map=()',\ 79 | 'magnetometer=()', 'microphone=()', 'midi=()',\ 80 | 'navigation-override=()', 'payment=()', 'picture-in-picture=()',\ 81 | 'publickey-credentials-get=()', 'screen-wake-lock=()', 'sync-xhr=()',\ 82 | 'usb=()', 'web-share=()', 'xr-spatial-tracking=()'} 83 | for line in pp_lines: 84 | values = (line.split(':')[1]).strip() 85 | ind = set(map(lambda x: x.strip(), values.split(','))) 86 | diff = pp_contrast.difference(ind) 87 | if diff: 88 | return ['fail', f'Permissions-Policy missing {" ".join(list(diff))}'] 89 | return ['pass'] 90 | else: 91 | return ['fail', 'No Permissions Policy headers found'] 92 | 93 | def cachecontrol_check(req): 94 | cc_lines = head_filter(req, 'Cache-Control') 95 | if cc_lines: 96 | for line in cc_lines: 97 | values = (line.split(':')[1]).strip() 98 | # Has to be both 'no-cache' and 'private'. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#dealing_with_outdated_implementations 99 | if not 'no-cache' in values.lower() and not 'private' in values.lower(): 100 | return ['fail', f'Cache headers set to {values}'] 101 | return ['pass'] 102 | else: 103 | return ['uncertain', 'No Cache-Control headers found'] 104 | 105 | def referrerpolicy_check(req): 106 | rp_lines = head_filter(req, 'Referrer-Policy') 107 | if rp_lines: 108 | for line in rp_lines: 109 | if 'unsafe' in (line.strip()).lower(): 110 | return ['fail', f'Referrer-Policy set to {(line.split(":")[1]).strip()}'] 111 | return ['pass', f'Referrer-Policy set to {(line.split(":")[1]).strip()}'] 112 | else: 113 | # Omitting the header defaults to a secure option: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 114 | return ['pass', "Headers omitted"] 115 | 116 | def contentsecuritypolicy_check(req): 117 | # Currently allows for Content-Security-Report-Only too. The tester has to check this when viewing the report. 118 | csp_lines = head_filter(req, 'Content-Security-Policy') 119 | if csp_lines: 120 | values = '\n'.join(csp_lines) 121 | return ['uncertain', f'Content security policy set to {values}'] 122 | else: 123 | return ['fail', 'No Content Security Policy found'] 124 | 125 | def parse(f): 126 | tree = ET.parse(f) 127 | root = tree.getroot() 128 | finale = {} 129 | for item in root: 130 | url = item.find('url').text 131 | response = item.find('response').text 132 | method = item.find('method').text 133 | if not response or method == "OPTIONS": 134 | continue 135 | raw_req = b64.b64decode(response) 136 | req = (raw_req.decode(errors="ignore")).split('\r\n') 137 | 138 | headers = [] 139 | for line in req: 140 | if not line: 141 | break 142 | else: 143 | headers.append(line) 144 | 145 | results = {} 146 | 147 | results['hsts'] = hsts_check(headers) 148 | results['x-frame-options'] = xframeoptions_check(headers) 149 | results['x-content-type-options'] = xcontenttypeoptions_check(headers) 150 | results['x-xss-protection'] = xxssprotection_check(headers) 151 | results['permissions-policy'] = permissionspolicy_check(headers) 152 | results['cache-control'] = cachecontrol_check(headers) 153 | results['referrer-policy'] = referrerpolicy_check(headers) 154 | results['content-security-policy'] = contentsecuritypolicy_check(headers) 155 | 156 | # OPTIONS doesn't need the other headers 157 | if method == "OPTIONS": 158 | for key in results.keys(): 159 | if key == 'hsts': 160 | pass 161 | else: 162 | results[key] = ['pass'] 163 | 164 | # This will overwrite duplicate entries. Sounds logical right now, but 165 | # keep it in mind. 166 | url = url.split('?')[0] 167 | finale[url] = results 168 | return finale 169 | 170 | def parse_md(): 171 | vkb = {} 172 | with open(Path(__file__).with_name("vkb.md"), 'r') as f: 173 | header = "" 174 | content = [] 175 | for line in f: 176 | line = line.strip() 177 | if not line: 178 | continue 179 | elif line.startswith("# "): 180 | if header and content: 181 | vkb[header] = content 182 | header = line[2:] 183 | content = [] 184 | else: 185 | header = line[2:] 186 | elif line.startswith("## "): 187 | if header and content: 188 | vkb[header] = content 189 | header = line[3:] 190 | content = [] 191 | else: 192 | header = line[3:] 193 | else: 194 | content.append(line) 195 | vkb[header] = content 196 | return vkb 197 | 198 | def to_html(vkb, table): 199 | code_blocks = re.compile(r"`(.*)`", flags=re.MULTILINE) 200 | li = re.compile(r"^\s?- (.+)$", flags=re.MULTILINE) 201 | href = re.compile(r"(https://\S*)", flags=re.MULTILINE) 202 | results = {} 203 | sections = ["Summary", "Background", "Recommendations", "Further Reading"] 204 | 205 | for key in vkb.keys(): 206 | if key in sections: 207 | header = key 208 | lines = [] 209 | for line in vkb[key]: 210 | line = code_blocks.sub(r"<code>\1</code>", line) 211 | line = li.sub(r"<li>\1</li>", line) 212 | line = href.sub(r'<a href="\1">\1</a>', line) 213 | lines.append(line) 214 | results[header] = lines 215 | else: 216 | lines = [f"<b>{key}</b>"] 217 | for line in vkb[key]: 218 | line = code_blocks.sub(r"<code>\1</code>", line) 219 | line = li.sub(r"<li>\1</li>", line) 220 | line = href.sub(r'<a href="\1">\1</a>', line) 221 | lines.append(line) 222 | results["Background"].extend(lines) 223 | 224 | for key in results.keys(): 225 | for i in range(len(results[key])): 226 | line = results[key][i] 227 | if line.startswith("

") or line.startswith("<li>"): 228 | continue 229 | else: 230 | results[key][i] = f"<p>{line}</p>" 231 | 232 | results["Further Reading"][0] = f"<ul>{results['Further Reading'][0]}" 233 | results["Further Reading"][1] = f"{results['Further Reading'][1]}" 234 | results["Background"].append(table) 235 | 236 | finale = {} 237 | 238 | for key in results.keys(): 239 | finale[key] = '\n'.join(results[key]) 240 | 241 | return finale 242 | 243 | def report(results, component_table): 244 | report = { 245 | 'hsts': True, 'x-frame-options': True, 'x-content-type-options': True, 'x-xss-protection': True,\ 246 | 'permissions-policy': True, 'referrer-policy': True, 'cache-control': True, 'content-security-policy': True 247 | } 248 | 249 | translation = { 250 | 'hsts': 'HTTP Strict Transport Security (HSTS)', 'x-frame-options': 'Clickjacking', 'x-content-type-options': 'Content Sniffing', 'x-xss-protection': 'X-XSS-Protection',\ 251 | 'permissions-policy': 'Permissions Policy', 'referrer-policy': 'Referrer Policy', 'cache-control': 'Cacheable HTTPS Response', 'content-security-policy': 'Content Security Policy (CSP)' 252 | } 253 | 254 | rec_translation = { 255 | 'hsts': ['Strict', 'HSTS'], 'x-frame-options': ['X-Frame-Options'], 'x-content-type-options': ['X-Content-Type-Options'], 'x-xss-protection': ['X-XSS-Protection'],\ 256 | 'permissions-policy': ['Permissions'], 'referrer-policy': ['Referrer'], 'cache-control': ['Cache'], 'content-security-policy': ['Content'] 257 | } 258 | 259 | for key in results.keys(): 260 | temp = results[key] 261 | for key2 in temp.keys(): 262 | if temp[key2][0] == "fail" or temp[key2][0] == "uncertain": 263 | report[key2] = False 264 | 265 | if all(report.values()): 266 | return {} 267 | 268 | vkb = parse_md() 269 | reading = vkb['Further Reading'] 270 | for key in report.keys(): 271 | if report[key]: 272 | vkb.pop(translation[key]) 273 | for v in rec_translation[key]: 274 | reading = list(filter(lambda x: not x.startswith(f"- {v}"), reading)) 275 | vkb['Further Reading'] = reading 276 | 277 | return to_html(vkb, component_table) 278 | 279 | def cache_report(report): 280 | passed = [] 281 | failed = [] 282 | 283 | for k in report.keys(): 284 | # We don't need to worry about JavaScript and Cascading Style Sheets. 285 | stripped_url = k.split('?')[0] 286 | file_endings = ['js', 'gif', 'jpg', 'png', 'css', 'woff2', 'woff'] 287 | if any([stripped_url.endswith(x) for x in file_endings]): 288 | continue 289 | elif report[k]['cache-control'][0] == 'pass': 290 | passed.append(k) 291 | else: 292 | failed.append(k) 293 | 294 | results = { 295 | 'passed': passed, 296 | 'failed': failed 297 | } 298 | 299 | return results 300 | 301 | def csp_report(report): 302 | result = {} 303 | for k in report.keys(): 304 | if report[k]['content-security-policy'][0] == "uncertain": 305 | values = report[k]['content-security-policy'][1][31:] 306 | result[values] = result.get(values, []) + [k] 307 | return result 308 | 309 | def print_results(output, client=""): 310 | ok = (' ' * 12) + '\u001b[32mOK\x1b[0m' 311 | nok = (' ' * 8) + '\u001b[31mNOT OK\x1b[0m' 312 | mok = '\u001b[33mPOTENTIALLY NOT OK\x1b[0m' 313 | 314 | if client: 315 | print(f"\u001b[33mSecurity header analysis for {client}:\x1b[0m") 316 | else: 317 | print("\u001b[33mSecurity header analysis:\x1b[0m") 318 | 319 | for key in output.keys(): 320 | print(f"\n\u001b[33m--> {key}:\x1b[0m") 321 | secure = output[key] 322 | if secure['hsts']: 323 | print(f"{'Secure Transport Security':<30}: {ok}") 324 | else: 325 | print(f"{'Secure Transport Security':<30}: {nok}") 326 | if secure['x-frame-options']: 327 | print(f"{'X-Frame-Options':<30}: {ok}") 328 | else: 329 | print(f"{'X-Frame-Options':<30}: {nok}") 330 | if secure['x-content-type-options']: 331 | print(f"{'X-Content-Type-Options':<30}: {ok}") 332 | else: 333 | print(f"{'X-Content-Type-Options':<30}: {nok}") 334 | if secure['content-security-policy']: 335 | print(f"{'Content-Security-Policy':<30}: {ok}") 336 | else: 337 | print(f"{'Content-Security-Policy':<30}: {nok}") 338 | if secure['x-xss-protection']: 339 | print(f"{'X-XSS-Protection':<30}: {ok}") 340 | else: 341 | print(f"{'X-XSS-Protection':<30}: {nok}") 342 | if secure['permissions-policy']: 343 | print(f"{'Permissions-Policy':<30}: {ok}") 344 | else: 345 | print(f"{'Permissions-Policy':<30}: {nok}") 346 | if secure['referrer-policy']: 347 | print(f"{'Referrer-Policy':<30}: {ok}") 348 | else: 349 | print(f"{'Referrer-Policy':<30}: {nok}") 350 | if secure['cache-control']: 351 | print(f"{'Cache-Control':<30}: {ok}") 352 | else: 353 | print(f"{'Cache-Control':<30}: {nok}") 354 | 355 | def restructure_domains(results): 356 | urls = sorted(list(results.keys())) 357 | domains = {} 358 | for url in urls: 359 | root = urlparse(url).netloc 360 | 361 | if domains.get(root, False): 362 | domains[root][url] = results[url] 363 | else: 364 | domains[root] = {url: results[url]} 365 | return domains 366 | 367 | def component_table(output): 368 | table_data = {} 369 | for domain in output: 370 | table_data[domain] = [] 371 | secure = output[domain] 372 | if not secure['hsts']: 373 | table_data[domain].append('Secure Transport Security') 374 | 375 | if not secure['x-frame-options']: 376 | table_data[domain].append('X-Frame-Options') 377 | 378 | if not secure['x-content-type-options']: 379 | table_data[domain].append('X-Content-Type-Options') 380 | 381 | if not secure['content-security-policy']: 382 | table_data[domain].append('Content-Security-Policy') 383 | 384 | if not secure['x-xss-protection']: 385 | table_data[domain].append('X-XSS-Protection') 386 | 387 | if not secure['permissions-policy']: 388 | table_data[domain].append('Permissions-Policy') 389 | 390 | if not secure['referrer-policy']: 391 | table_data[domain].append('Referrer-Policy') 392 | 393 | if not secure['cache-control']: 394 | table_data[domain].append('Cache-Control') 395 | 396 | result = ["<p><b>Affected Components</p></b><ul>"] 397 | for key in table_data.keys(): 398 | result.append(f"<li>{key}</li><ul>") 399 | for item in table_data[key]: 400 | result.append(f"<li>{item}</li>") 401 | result.append("</ul>") 402 | result.append("</ul>") 403 | return '\n'.join(result) 404 | 405 | def print_samurai(bow=True): 406 | samurai = """\u001b[32m 407 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 408 | ░░░░░░░░░░░░░░░░░▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░▒▓▓▓▓▒░░░░░░░░░░░░░░░░ 409 | ░░░░░░░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░░░░▒▓▓▓▓▓▒░░░░░░░░░░░░░░░░░ 410 | ░░░░░░░░░░░░░░░░░░▒▓▓▓▓▓▓▓░░░░░░░░░░░░▒▓▒▓▓▓▓▒░░░░░░░░░░░░░░░░░░ 411 | ░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓░░░░░░░▒▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░ 412 | ░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒░░░░░░▒▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░ 413 | ░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░ 414 | ░░░░░░░░░░░░░░░░░░░▓▓▓▓▒░░░░░░░░░░░░░░░░▓▓▓▓▒░░░░░░░░░░░░░░░░░░░ 415 | ░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░▓▓▓▓▒░░░░░░░░░░░░░░░░░░░ 416 | ░░░░░░░░░░░░░░░░░░░▒▓▓▓░░░▒▒▒▒▓█▓█▓▒▒▒░░▒▓▓▓░░░░░░░░░░░░░░░░░░░░ 417 | ░░░░░░░░░░░░░░░░░░░▒▓▓▓▒▓████▓▓▓▓▓▓█████▓▓▓▓░░░░░░░░░░░░░░░░░░░░ 418 | ░░░░░░░░░░░░░░░░░░░░▓▓▓▓███▓▓▓▓▓▓▓▓▓▓██▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░ 419 | ░░░░░░░░░░░░░░░░░░░░▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░ 420 | ░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓█▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░ 421 | ░░░░░░░░░░░░░▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓██▓▒▒░░░░░░░░░░░░░░░░░ 422 | ░░░░░░░░░░░░░▒▓▓▒▓▓▓█▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓██▓▓▓▓▓▓▓▒░░░░░░░░░░░░ 423 | ░░░░░░░░░░░░░░▒▒▒▒▒▒▓█▓▓▓▓▒▒▒▒▒▒▒▒▒▒▓▒▓▓▓███▓▓▒▒▓▓▒░░░░░░░░░░░░░ 424 | ░░░░░░░░░░░░░░░▒▒▒▒▓█▓██▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓██▓▓▓▓▓▒▒▒▒░░░░░░░░░░░░░░ 425 | ░░░░░░░░░░░░░░▒█▓▓██▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▒░░░░░░░░░░░░░░ 426 | ░░░░░░░░░░░░░▓▓██████▓▓▓▓▓▓▓▓▒▒▒▒▒▒▓▓▓▓▓▓▓▓█████▓▓▒░░░░░░░░░░░░░ 427 | ░░░░░░░░░░░░▓▓██████████████▓▓▓▓▓▓▓▓▓████████████▓▓▓░░░░░░░░░░░░ 428 | ░░░░░░░░░░▒▓▓█████████████████▓▓▓█████████████████▓▓▓░░░░░░░░░░░ 429 | ░░░░░░░░░▒▓▓████▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓██████▓▓▓▒░░░░░░░░░ 430 | ░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▓▓▓░░░░░░░░░ 431 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 432 | """ 433 | names = ["夜叉", "YASHA", "やしゃ"] 434 | version = "0.b" 435 | print(f"{samurai:^45}") 436 | print(f"{random.choice(names)+' '+version:^64s}\x1b[0m\n") 437 | 438 | def yasha(burp_file, log=False): 439 | parcel = {} 440 | 441 | with open(burp_file, 'r') as f: 442 | original_results = parse(f) 443 | 444 | domains = restructure_domains(original_results) 445 | 446 | if log: 447 | parcel['log']= json.dumps(original_results) 448 | 449 | output = {} 450 | 451 | for domain in domains.keys(): 452 | secure = { 453 | 'hsts': True, 'x-frame-options': True, 'x-content-type-options': True, 'x-xss-protection': True,\ 454 | 'permissions-policy': True, 'referrer-policy': True, 'cache-control': True, 'content-security-policy': True 455 | } 456 | 457 | entry = domains[domain] 458 | for key in entry.keys(): 459 | temp = entry[key] 460 | for key2 in temp.keys(): 461 | if temp[key2][0] != "pass": 462 | secure[key2] = False 463 | 464 | results = domains[domain] 465 | print(f"Analysing \u001b[34m{domain}\x1b[0m") 466 | 467 | cache_results = cache_report(results) 468 | if cache_results['failed'] or cache_results['passed']: 469 | print("\t\u001b[33mDo the right URL endpoints have caching headers according to the following?\x1b[0m") 470 | if len(cache_results['passed']) + len(cache_results['failed']) > 10: 471 | page = ["

Cache Headers OK:

Cache Headers NOT OK

') 475 | for entry in cache_results['failed']: 476 | page.append(f"
  • {entry}
  • ") 477 | page.append("") 478 | with open("cache_report.htm", 'w') as f: 479 | f.write(''.join(page)) 480 | print("\tA large number of URLs noted. Check cache_report.htm.") 481 | else: 482 | print("\u001b[32mCache Headers OK:\x1b[0m") 483 | for item in cache_results['passed']: 484 | print(f"\t{item}") 485 | print("\u001b[31mCache Headers NOT OK:\x1b[0m") 486 | for item in cache_results['failed']: 487 | print(f"\t{item}") 488 | while True: 489 | try: 490 | answer = input("\t[y/n] > ")[0].lower() 491 | except IndexError: 492 | continue 493 | 494 | if answer == 'y': 495 | secure['cache-control'] = True 496 | break 497 | elif answer == 'n': 498 | secure['cache-control'] = False 499 | break 500 | else: 501 | continue 502 | 503 | # It's a direct fail if some pages don't have it. Will be 'fail' in this case, 504 | # and 'uncertain' otherwise. 505 | if not all([results[key]['content-security-policy'][0] != 'fail' for key in results.keys()]): 506 | pass 507 | else: 508 | csp_results = csp_report(results) 509 | if csp_results: 510 | page = [] 511 | for (i, k) in enumerate(csp_results.keys()): 512 | page.append("") 513 | page.append(f"

    CSP Value #{i}

    CSP Value

    ") 514 | page.append(f"

    {k.split(':')[0]}

    Check in Google\'s CSP Evaluator (Warning: external site)') 519 | page.append("

    URLs

    ') 524 | with open("csp_report.htm", 'w') as f: 525 | f.write(''.join(page)) 526 | print("\t\u001b[33mAre the CSP headers secure in csp_report.htm?\x1b[0m") 527 | while True: 528 | try: 529 | answer = input("\t[y/n] > ")[0].lower() 530 | except IndexError: 531 | continue 532 | 533 | if answer == 'y': 534 | secure['content-security-policy'] = True 535 | break 536 | elif answer == 'n': 537 | secure['content-security-policy'] = False 538 | break 539 | else: 540 | continue 541 | 542 | output[domain] = secure 543 | 544 | parcel['output'] = output 545 | parcel['report'] = ''.join([f'\n\n

    {x[0]}

    \n\n{x[1]}' for x in (report(original_results, component_table=component_table(output))).items()]) 546 | js = '' 547 | parcel['report'] = parcel['report'] + js 548 | 549 | return(parcel) 550 | 551 | if __name__ == "__main__": 552 | parser = argparse.ArgumentParser( 553 | prog="Yasha", 554 | description="Yet Another Security Header Analyser" 555 | ) 556 | parser.add_argument('-f', '--file', action='store', required=True, help="Burp exported requests/responses.") 557 | parser.add_argument('-c', '--client', action='store', help="Optional client name for display for screenshot.") 558 | parser.add_argument('-l', '--log', action="store_true", help="Write a JSON log.") 559 | args = parser.parse_args() 560 | 561 | print_samurai(bow=True) 562 | 563 | parcel = yasha(args.file, log=args.log) 564 | 565 | with open('reporting.htm', 'w') as f: 566 | f.write(parcel['report']) 567 | print("\n\n\u001b[33mReporting output written to reporting.htm\x1b[0m\n") 568 | 569 | if 'log' in parcel: 570 | with open('yasha_log.json', 'w') as f: 571 | f.write(parcel['log']) 572 | print("\u001b[33mLogs written to yasha_log.json\x1b[0m") 573 | print_results(parcel['output'], client=args.client) --------------------------------------------------------------------------------