File size: 13,710 Bytes
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
"""

csp_scanner.py — Content Security Policy (CSP) Deep Auditor

============================================================

Parses and analyses the Content-Security-Policy (and CSP-Report-Only) header:

  - Detects missing CSP entirely

  - Flags unsafe-inline / unsafe-eval in script-src

  - Flags wildcard (*) sources

  - Detects missing critical directives

  - Identifies CSP bypass vectors (data:, blob:, http: schemes)

  - Checks for report-uri / report-to configuration

  - Scores overall CSP strength

"""
import re
import urllib.request
import urllib.error
from scanners.base_scanner import BaseScanner

CRITICAL_DIRECTIVES = [
    "default-src", "script-src", "style-src", "img-src",
    "connect-src", "font-src", "object-src", "frame-ancestors",
]

BYPASS_SCHEMES = ["data:", "blob:", "http:", "javascript:"]

CDNS_ALLOWLISTED_BUT_RISKY = [
    "cdn.jsdelivr.net", "unpkg.com", "cdnjs.cloudflare.com",
    "ajax.googleapis.com", "code.jquery.com",
]


class CspScanner(BaseScanner):
    SCANNER_NAME = "Content Security Policy (CSP) Auditor"
    _SCANNER_KEY = "csp"

    def __init__(self, scan_id, target, domain, **kwargs):
        super().__init__(scan_id, target, domain, **kwargs)

    def run(self) -> list:
        self.log("INFO", f"[CSP] Auditing Content-Security-Policy on {self.target}...")
        try:
            headers = self._fetch_headers()
            csp = headers.get("content-security-policy", "")
            csp_ro = headers.get("content-security-policy-report-only", "")

            if not csp and not csp_ro:
                self.log("WARNING", "[CSP] No Content-Security-Policy header found!")
                self.add_vuln(
                    title="Content Security Policy (CSP) Not Configured",
                    severity="High",
                    category="Content Security Policy",
                    cvss_score=7.5,
                    description=f"The target `{self.target}` does not serve a "
                        "Content-Security-Policy header. Without CSP, the browser has "
                        "no policy to enforce, making XSS attacks significantly more "
                        "damaging as injected scripts run with full page privileges.",
                    remediation="Implement a strict CSP:\n"
                        "Content-Security-Policy: default-src 'self'; "
                        "script-src 'self' 'nonce-{random}'; "
                        "object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
                    evidence="No Content-Security-Policy header in response",
                    request_details=f"GET {self.target}",
                    response_details="Missing Content-Security-Policy header",
                    confidence="Confirmed",
                )
                return self.vulns

            active_csp = csp or csp_ro
            label = "CSP-Report-Only" if not csp else "CSP"
            self.log("INFO", f"[CSP] {label} header found. Parsing directives...")

            if not csp and csp_ro:
                self.add_vuln(
                    title="CSP Deployed in Report-Only Mode (Not Enforced)",
                    severity="Medium",
                    category="Content Security Policy",
                    cvss_score=5.3,
                    description="A Content-Security-Policy-Report-Only header is present but "
                        "no enforcing CSP header exists. Report-Only mode does not prevent "
                        "attacks — it only sends violation reports.",
                    remediation="Promote the policy to Content-Security-Policy once verified.",
                    evidence="Content-Security-Policy-Report-Only present, no enforcing CSP",
                    request_details=f"GET {self.target}",
                    response_details="Report-Only mode CSP detected",
                    confidence="High",
                )

            directives = self._parse_csp(active_csp)
            self._audit_directives(directives, active_csp)

        except Exception as e:
            self.log("ERROR", f"[CSP] Audit error: {e}")

        self.log(
            "SUCCESS" if not self.vulns else "WARNING",
            f"[CSP] Audit complete. {len(self.vulns)} issue(s) found.",
        )
        return self.vulns

    def _fetch_headers(self) -> dict:
        body, status, resp_headers = self._make_request(
            self.target, return_response_obj=True
        )
        if resp_headers:
            return {k.lower(): v for k, v in resp_headers.items()}
        return {}

    @staticmethod
    def _parse_csp(csp: str) -> dict:
        directives = {}
        for part in csp.split(";"):
            part = part.strip()
            if not part:
                continue
            tokens = part.split()
            if tokens:
                directives[tokens[0].lower()] = tokens[1:]
        return directives

    def _audit_directives(self, directives: dict, raw_csp: str):
        script_src = directives.get("script-src",
                     directives.get("default-src", []))

        if "'unsafe-inline'" in script_src:
            self.add_vuln(
                title="CSP Allows 'unsafe-inline' in script-src",
                severity="High",
                category="Content Security Policy",
                cvss_score=7.4,
                description="The CSP `script-src` directive includes `'unsafe-inline'`, "
                    "which allows inline `<script>` blocks and `onclick=` handlers. "
                    "This completely undermines XSS protection provided by CSP.",
                remediation="Replace 'unsafe-inline' with nonce-based or hash-based "
                    "script allowlisting:\n"
                    "script-src 'self' 'nonce-{server_generated_random}';",
                evidence=f"script-src contains 'unsafe-inline': {script_src}",
                payload="'unsafe-inline' in CSP",
                request_details=f"GET {self.target}",
                response_details=f"CSP directives: {directives}",
                confidence="Confirmed",
            )
        else:
            self.log("SUCCESS", "[CSP] 'unsafe-inline' not found in script-src")

        if "'unsafe-eval'" in script_src:
            self.add_vuln(
                title="CSP Allows 'unsafe-eval' in script-src",
                severity="Medium",
                category="Content Security Policy",
                cvss_score=5.3,
                description="The CSP `script-src` allows `eval()`, `new Function()`, "
                    "and similar dynamic code execution. This weakens XSS mitigation.",
                remediation="Remove 'unsafe-eval'. Refactor code to avoid eval(). "
                    "Use JSON.parse() instead of eval() for JSON data.",
                evidence=f"script-src contains 'unsafe-eval': {script_src}",
                payload="'unsafe-eval' in CSP",
                request_details=f"GET {self.target}",
                response_details=f"CSP directives: {directives}",
                confidence="Confirmed",
            )

        for directive, sources in directives.items():
            if "*" in sources:
                self.add_vuln(
                    title=f"CSP Wildcard (*) Source in '{directive}'",
                    severity="High",
                    category="Content Security Policy",
                    cvss_score=7.5,
                    description=f"The `{directive}` directive allows any origin (`*`), "
                        "effectively disabling the protection for this resource type.",
                    remediation=f"Replace `*` in `{directive}` with explicit, "
                        "trusted origins only.",
                    evidence=f"{directive}: {' '.join(sources)} contains wildcard",
                    payload=f"{directive} *",
                    request_details=f"GET {self.target}",
                    response_details=f"CSP directive {directive} allows *",
                    confidence="Confirmed",
                )

        for scheme in BYPASS_SCHEMES:
            if scheme in raw_csp:
                self.add_vuln(
                    title=f"CSP Bypass via '{scheme}' Scheme Allowed",
                    severity="Medium",
                    category="Content Security Policy",
                    cvss_score=5.5,
                    description=f"The CSP permits the `{scheme}` scheme, which is commonly "
                        "abused for CSP bypass techniques (especially data: URIs for script execution).",
                    remediation=f"Remove `{scheme}` from all CSP directives unless strictly required.",
                    evidence=f"CSP contains bypass scheme: {scheme}",
                    payload=f"{scheme} in CSP",
                    request_details=f"GET {self.target}",
                    response_details=f"CSP allows scheme: {scheme}",
                    confidence="High",
                )

        obj_src = directives.get("object-src", directives.get("default-src", []))
        if not obj_src or (obj_src != ["'none'"] and "'none'" not in obj_src):
            self.add_vuln(
                title="CSP Missing 'object-src none' Directive",
                severity="Medium",
                category="Content Security Policy",
                cvss_score=5.3,
                description="Without `object-src 'none'`, the browser may load Flash, "
                    "Java applets, or other plugins that bypass script-src restrictions.",
                remediation="Add: object-src 'none'; to your CSP.",
                evidence=f"object-src directive: {obj_src}",
                request_details=f"GET {self.target}",
                response_details="object-src not set to 'none'",
                confidence="High",
            )

        if "frame-ancestors" not in directives:
            self.add_vuln(
                title="CSP Missing 'frame-ancestors' Directive (Clickjacking)",
                severity="Medium",
                category="Content Security Policy",
                cvss_score=5.4,
                description="The CSP does not include `frame-ancestors`, which controls "
                    "whether the page can be embedded in iframes. Without it, clickjacking "
                    "attacks remain possible if X-Frame-Options is also absent.",
                remediation="Add: frame-ancestors 'none'; (or 'self' if same-origin framing needed)",
                evidence="Directives present: " + ", ".join(directives.keys()),
                request_details=f"GET {self.target}",
                response_details="frame-ancestors directive missing",
                confidence="High",
            )
        else:
            self.log("SUCCESS", "[CSP] frame-ancestors directive present")

        if "base-uri" not in directives:
            self.add_vuln(
                title="CSP Missing 'base-uri' Directive",
                severity="Low",
                category="Content Security Policy",
                cvss_score=3.5,
                description="Without `base-uri`, an attacker who injects a `<base>` tag "
                    "can redirect all relative URLs to an attacker-controlled origin.",
                remediation="Add: base-uri 'self'; to your CSP.",
                evidence="Directives present: " + ", ".join(directives.keys()),
                request_details=f"GET {self.target}",
                response_details="base-uri directive missing",
                confidence="High",
            )

        if "report-uri" not in directives and "report-to" not in directives:
            self.add_vuln(
                title="CSP Has No Reporting Endpoint Configured",
                severity="Low",
                category="Content Security Policy",
                cvss_score=2.0,
                description="The CSP does not include a `report-uri` or `report-to` "
                    "directive. Without reporting, CSP violations go undetected.",
                remediation="Add: report-uri /csp-report-endpoint; "
                    "to receive violation reports from browsers.",
                evidence="Directives present: " + ", ".join(directives.keys()),
                request_details=f"GET {self.target}",
                response_details="No report-uri or report-to directive",
                confidence="High",
            )
        else:
            self.log("SUCCESS", "[CSP] CSP reporting endpoint configured")

        for cdn in CDNS_ALLOWLISTED_BUT_RISKY:
            if cdn in raw_csp:
                self.add_vuln(
                    title=f"CSP Allowlists Risky CDN: {cdn}",
                    severity="Low",
                    category="Content Security Policy",
                    cvss_score=3.5,
                    description=f"The CSP allowlists `{cdn}`. Public CDNs host thousands "
                        "of packages; if any contain malicious code or are compromised, "
                        "your CSP provides no protection.",
                    remediation="Pin specific script hashes instead of trusting entire CDN origins. "
                        "Use Subresource Integrity (SRI) for all external scripts.",
                    evidence=f"CDN {cdn} found in CSP allowlist",
                    payload=cdn,
                    request_details=f"GET {self.target}",
                    response_details=f"CSP allowlists {cdn}",
                    confidence="Medium",
                )