Skip to content

Commit a390f16

Browse files
author
ThodorhsPerros
committed
Add three new security header analyzers
Implement analyzers for legacy browser-specific security headers: - X-XSS-Protection: Validates XSS filter configuration (recommends disabling) - X-Download-Options: Validates IE download execution prevention - X-Permitted-Cross-Domain-Policies: Validates Flash/PDF cross-domain policies All analyzers follow OWASP and Mozilla best practices: - X-XSS-Protection: Best practice is '0' (explicitly disabled) - X-Download-Options: Requires 'noopen' value - X-Permitted-Cross-Domain-Policies: Best is 'none', acceptable is 'master-only' Changes: - Add sha/analyzers/x_xss_protection.py with full validation logic - Add sha/analyzers/x_download_options.py with noopen validation - Add sha/analyzers/x_permitted_cross_domain_policies.py with policy validation - Register all 3 analyzers in sha/analyzers/__init__.py - Update docs/SecurityHeadersBestPractices.md with detailed header documentation
1 parent c7a7a79 commit a390f16

File tree

5 files changed

+493
-1
lines changed

5 files changed

+493
-1
lines changed

docs/SecurityHeadersBestPractices.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,92 @@ Without Referrer-Policy, clicking any external link would send this full URL (in
163163

164164
---
165165

166+
## 6. X-XSS-Protection
167+
168+
**Purpose:** Legacy header that controlled browser XSS filters in Internet Explorer, Chrome, and Safari. Now deprecated in modern browsers.
169+
170+
**Best Practice:**
171+
```
172+
X-XSS-Protection: 0
173+
```
174+
Explicitly disables the XSS filter. This is the modern recommendation because these filters can introduce XSS vulnerabilities in otherwise safe websites.
175+
176+
**Acceptable:**
177+
Not setting the header at all is acceptable for modern applications that implement a strong Content-Security-Policy.
178+
179+
**Bad/Missing:**
180+
- `X-XSS-Protection: 1` (enables the filter without mode=block, can create vulnerabilities)
181+
- `X-XSS-Protection: 1; mode=block` (legacy approach, filter can introduce bugs)
182+
183+
**Severity if Missing:** Low
184+
185+
**Reasoning:** Modern browsers have removed XSS filter functionality, and Content-Security-Policy provides superior protection. However, explicitly setting `0` prevents older browsers from using potentially buggy XSS filters. OWASP recommends either not setting this header or explicitly disabling it with `0` to avoid filter-based vulnerabilities.
186+
187+
**References:**
188+
- [OWASP: X-XSS-Protection](https://owasp.org/www-project-secure-headers/)
189+
- [MDN: X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection)
190+
191+
---
192+
193+
## 7. X-Download-Options
194+
195+
**Purpose:** Internet Explorer 8+ specific header that prevents the browser from executing downloaded HTML files in the context of the site. Prevents Same Origin Policy violations during file downloads.
196+
197+
**Best Practice:**
198+
```
199+
X-Download-Options: noopen
200+
```
201+
Forces users to save the file before opening it, preventing execution in the site's security context.
202+
203+
**Acceptable:**
204+
Same as best practice. This header has only one valid value: `noopen`.
205+
206+
**Bad/Missing:**
207+
- Header not present when serving user-controllable HTML content with `Content-Disposition: attachment`
208+
- Any value other than `noopen`
209+
210+
**Severity if Missing:** Low
211+
212+
**Reasoning:** When a user directly opens a downloaded HTML file in IE, scripts in that file can access cookies from the originating domain. This violates the Same Origin Policy by allowing the downloaded file to execute as if it were part of the website. Setting this header forces the save-then-open workflow, ensuring downloaded files execute in a local context. While IE-specific and legacy, this header is still recommended when serving downloadable HTML content.
213+
214+
**References:**
215+
- [OWASP: X-Download-Options](https://owasp.org/www-project-secure-headers/)
216+
- [Microsoft: X-Download-Options](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/)
217+
218+
---
219+
220+
## 8. X-Permitted-Cross-Domain-Policies
221+
222+
**Purpose:** Controls whether Adobe Flash Player, Adobe Acrobat, or PDF documents can load cross-domain policy files from the web server. Prevents untrusted Flash/PDF content from accessing site data.
223+
224+
**Best Practice:**
225+
```
226+
X-Permitted-Cross-Domain-Policies: none
227+
```
228+
Completely prohibits Flash and PDF clients from loading any cross-domain policy files. Most secure option.
229+
230+
**Acceptable:**
231+
```
232+
X-Permitted-Cross-Domain-Policies: master-only
233+
```
234+
Allows only the master policy file (`/crossdomain.xml`) to be loaded. Acceptable if you need to support legacy Flash/PDF content with controlled cross-domain access.
235+
236+
**Bad/Missing:**
237+
- Header not present (allows policy files from any location)
238+
- `all`: Allows policy files from anywhere on the server (very insecure)
239+
- `by-content-type`: Allows policy files served with `Content-Type: text/x-cross-domain-policy` (too permissive)
240+
- `by-ftp-filename`: Allows policy files with specific FTP filenames (legacy, insecure)
241+
242+
**Severity if Missing:** Medium
243+
244+
**Reasoning:** While Adobe Flash is deprecated (EOL December 2020), this header remains relevant for security audits and defense-in-depth. Without proper configuration, attackers could place a malicious `crossdomain.xml` file on your server, allowing Flash/PDF content from other domains to read your application's data and bypass CSRF protections. Many security scanners still check for this header. Setting it to `none` is a simple, zero-cost protection.
245+
246+
**References:**
247+
- [OWASP: X-Permitted-Cross-Domain-Policies](https://owasp.org/www-project-secure-headers/)
248+
- [Adobe: Cross-Domain Policy](https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html)
249+
250+
---
251+
166252
## Summary Table
167253

168254
| Header | Severity if Missing | Ease of Implementation | Security Impact |
@@ -171,4 +257,7 @@ Without Referrer-Policy, clicking any external link would send this full URL (in
171257
| X-Frame-Options | High | Very Easy | High |
172258
| X-Content-Type-Options | Medium-High | Very Easy | High |
173259
| CSP | Critical | Hard | Very High |
174-
| Referrer-Policy | High | Very Easy | High |
260+
| Referrer-Policy | High | Very Easy | High |
261+
| X-XSS-Protection | Low | Very Easy | Low |
262+
| X-Download-Options | Low | Very Easy | Low |
263+
| X-Permitted-Cross-Domain-Policies | Medium | Very Easy | Medium |

sha/analyzers/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from . import coep
1818
from . import coop
1919
from . import corp
20+
from . import x_xss_protection
21+
from . import x_download_options
22+
from . import x_permitted_cross_domain_policies
2023

2124

2225
# Registry mapping header keys to analyzer functions
@@ -30,6 +33,9 @@
3033
coep.HEADER_KEY: coep.analyze,
3134
coop.HEADER_KEY: coop.analyze,
3235
corp.HEADER_KEY: corp.analyze,
36+
x_xss_protection.HEADER_KEY: x_xss_protection.analyze,
37+
x_download_options.HEADER_KEY: x_download_options.analyze,
38+
x_permitted_cross_domain_policies.HEADER_KEY: x_permitted_cross_domain_policies.analyze,
3339
}
3440

3541
# Registry mapping header keys to configurations
@@ -43,6 +49,9 @@
4349
coep.HEADER_KEY: coep.CONFIG,
4450
coop.HEADER_KEY: coop.CONFIG,
4551
corp.HEADER_KEY: corp.CONFIG,
52+
x_xss_protection.HEADER_KEY: x_xss_protection.CONFIG,
53+
x_download_options.HEADER_KEY: x_download_options.CONFIG,
54+
x_permitted_cross_domain_policies.HEADER_KEY: x_permitted_cross_domain_policies.CONFIG,
4655
}
4756

4857

@@ -100,4 +109,7 @@ def get_config(header_key: str) -> Dict[str, Any]:
100109
"coep",
101110
"coop",
102111
"corp",
112+
"x_xss_protection",
113+
"x_download_options",
114+
"x_permitted_cross_domain_policies",
103115
]
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
X-Download-Options Header Analyzer.
3+
4+
This module contains configuration and analysis logic for the
5+
X-Download-Options header which prevents Internet Explorer from
6+
executing downloaded HTML files in the site's security context.
7+
"""
8+
9+
from typing import Any, Dict, Optional
10+
11+
from ..config import STATUS_BAD, STATUS_GOOD, STATUS_MISSING
12+
13+
14+
HEADER_KEY = "x-download-options"
15+
16+
CONFIG = {
17+
"display_name": "X-Download-Options",
18+
"severity_missing": "low",
19+
"description": "Prevents IE from executing downloads in site context",
20+
"validation": {
21+
"required_value": "noopen",
22+
},
23+
"messages": {
24+
STATUS_GOOD: "X-Download-Options is properly configured",
25+
STATUS_BAD: "X-Download-Options has incorrect value",
26+
STATUS_MISSING: "X-Download-Options header is missing - IE may execute downloads in site context",
27+
},
28+
"recommendations": {
29+
"missing": "Add: X-Download-Options: noopen",
30+
"wrong_value": "Set value to: noopen",
31+
},
32+
}
33+
34+
35+
def analyze(value: Optional[str]) -> Dict[str, Any]:
36+
"""
37+
Analyze X-Download-Options header.
38+
39+
Validation rules:
40+
- Missing: Low severity (IE-specific, legacy)
41+
- "noopen": Good
42+
- Other values: Bad
43+
44+
Args:
45+
value: Header value or None if missing
46+
47+
Returns:
48+
Finding dictionary with keys:
49+
- header_name: str
50+
- status: str (good/acceptable/bad/missing)
51+
- severity: str (critical/high/medium/low/info)
52+
- message: str
53+
- actual_value: str or None
54+
- recommendation: str or None
55+
"""
56+
header_name = CONFIG["display_name"]
57+
58+
# Missing header
59+
if value is None:
60+
return {
61+
"header_name": header_name,
62+
"status": STATUS_MISSING,
63+
"severity": CONFIG["severity_missing"],
64+
"message": CONFIG["messages"][STATUS_MISSING],
65+
"actual_value": None,
66+
"recommendation": CONFIG["recommendations"]["missing"],
67+
}
68+
69+
value_lower = value.strip().lower()
70+
71+
# Check for correct value
72+
if value_lower == CONFIG["validation"]["required_value"]:
73+
return {
74+
"header_name": header_name,
75+
"status": STATUS_GOOD,
76+
"severity": "info",
77+
"message": CONFIG["messages"][STATUS_GOOD],
78+
"actual_value": value,
79+
"recommendation": None,
80+
}
81+
82+
# Empty value
83+
if not value_lower:
84+
return {
85+
"header_name": header_name,
86+
"status": STATUS_BAD,
87+
"severity": CONFIG["severity_missing"],
88+
"message": f"{CONFIG['messages'][STATUS_BAD]} - empty value",
89+
"actual_value": value,
90+
"recommendation": CONFIG["recommendations"]["wrong_value"],
91+
}
92+
93+
# Incorrect value
94+
return {
95+
"header_name": header_name,
96+
"status": STATUS_BAD,
97+
"severity": CONFIG["severity_missing"],
98+
"message": f"{CONFIG['messages'][STATUS_BAD]} - "
99+
f"unknown value '{value}', should be 'noopen'",
100+
"actual_value": value,
101+
"recommendation": CONFIG["recommendations"]["wrong_value"],
102+
}

0 commit comments

Comments
 (0)