@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# CORS Remediation Playbook
|
|
2
|
+
|
|
3
|
+
## Pattern 1 — Replace reflection with allow-list
|
|
4
|
+
|
|
5
|
+
### nginx
|
|
6
|
+
|
|
7
|
+
```nginx
|
|
8
|
+
map $http_origin $cors_origin {
|
|
9
|
+
default "";
|
|
10
|
+
"~^https://app\.example\.com$" "$http_origin";
|
|
11
|
+
"~^https://admin\.example\.com$" "$http_origin";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
server {
|
|
15
|
+
location /api/ {
|
|
16
|
+
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
17
|
+
add_header Access-Control-Allow-Credentials true always;
|
|
18
|
+
add_header Vary Origin always;
|
|
19
|
+
if ($request_method = OPTIONS) {
|
|
20
|
+
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
|
|
21
|
+
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
|
|
22
|
+
add_header Access-Control-Max-Age 3600 always;
|
|
23
|
+
return 204;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `map` directive evaluates each allowed origin pattern. If none match, `$cors_origin` is empty and the Allow-Origin header is sent empty (browser rejects, request blocked).
|
|
30
|
+
|
|
31
|
+
### Express (Node.js)
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const cors = require('cors');
|
|
35
|
+
app.use('/api', cors({
|
|
36
|
+
origin: ['https://app.example.com', 'https://admin.example.com'],
|
|
37
|
+
credentials: true,
|
|
38
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
39
|
+
allowedHeaders: ['Authorization', 'Content-Type'],
|
|
40
|
+
maxAge: 3600,
|
|
41
|
+
}));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `cors` middleware sets `Vary: Origin` automatically; the `origin` array does exact-match against the request Origin.
|
|
45
|
+
|
|
46
|
+
### Spring (Java)
|
|
47
|
+
|
|
48
|
+
```java
|
|
49
|
+
@Configuration
|
|
50
|
+
public class WebConfig implements WebMvcConfigurer {
|
|
51
|
+
@Override
|
|
52
|
+
public void addCorsMappings(CorsRegistry registry) {
|
|
53
|
+
registry.addMapping("/api/**")
|
|
54
|
+
.allowedOrigins("https://app.example.com", "https://admin.example.com")
|
|
55
|
+
.allowedMethods("GET", "POST", "PUT", "DELETE")
|
|
56
|
+
.allowedHeaders("Authorization", "Content-Type")
|
|
57
|
+
.allowCredentials(true)
|
|
58
|
+
.maxAge(3600);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### FastAPI (Python)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
67
|
+
|
|
68
|
+
app.add_middleware(
|
|
69
|
+
CORSMiddleware,
|
|
70
|
+
allow_origins=["https://app.example.com", "https://admin.example.com"],
|
|
71
|
+
allow_credentials=True,
|
|
72
|
+
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
73
|
+
allow_headers=["Authorization", "Content-Type"],
|
|
74
|
+
max_age=3600,
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Rails 7
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# config/initializers/cors.rb
|
|
82
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
|
83
|
+
allow do
|
|
84
|
+
origins 'app.example.com', 'admin.example.com' # exact list
|
|
85
|
+
resource '/api/*',
|
|
86
|
+
headers: :any,
|
|
87
|
+
methods: [:get, :post, :put, :delete],
|
|
88
|
+
credentials: true,
|
|
89
|
+
max_age: 3600
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Pattern 2 — Fix subdomain pattern matching
|
|
95
|
+
|
|
96
|
+
### Wrong (JavaScript)
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
if (origin.endsWith('.example.com')) { allow(origin); }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Matches `evil.example.com.attacker.com`.
|
|
103
|
+
|
|
104
|
+
### Right (JavaScript)
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const url = new URL(origin);
|
|
108
|
+
const trustedHosts = ['example.com'];
|
|
109
|
+
if (
|
|
110
|
+
trustedHosts.includes(url.hostname) ||
|
|
111
|
+
trustedHosts.some(h => url.hostname.endsWith('.' + h))
|
|
112
|
+
) {
|
|
113
|
+
allow(origin);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `new URL()` parse + `hostname` extraction + leading-dot check eliminates the suffix-string bypass.
|
|
118
|
+
|
|
119
|
+
### Right (Python)
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from urllib.parse import urlparse
|
|
123
|
+
|
|
124
|
+
TRUSTED_HOSTS = {'example.com'}
|
|
125
|
+
|
|
126
|
+
def is_origin_allowed(origin: str) -> bool:
|
|
127
|
+
try:
|
|
128
|
+
hostname = urlparse(origin).hostname
|
|
129
|
+
except Exception:
|
|
130
|
+
return False
|
|
131
|
+
if hostname in TRUSTED_HOSTS:
|
|
132
|
+
return True
|
|
133
|
+
return any(hostname.endswith('.' + h) for h in TRUSTED_HOSTS)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Pattern 3 — Remove Allow-Origin:null trust
|
|
137
|
+
|
|
138
|
+
If your allow-list contains the string `'null'`, remove it. There's no legitimate cross-origin sender of Origin:null worth trusting.
|
|
139
|
+
|
|
140
|
+
Edge case: if you have a legitimate use case for sandboxed-iframe access to a specific endpoint, design around it explicitly (e.g., a separate auth-token-in-URL flow); do not extend CORS trust to Origin:null.
|
|
141
|
+
|
|
142
|
+
## Pattern 4 — Always set Vary:Origin when per-origin
|
|
143
|
+
|
|
144
|
+
### nginx (global for /api/)
|
|
145
|
+
|
|
146
|
+
```nginx
|
|
147
|
+
location /api/ {
|
|
148
|
+
add_header Vary Origin always;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Caddy
|
|
153
|
+
|
|
154
|
+
```caddy
|
|
155
|
+
example.com {
|
|
156
|
+
header /api/* Vary Origin
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### CDN-level (CloudFlare Workers, Fastly VCL)
|
|
161
|
+
|
|
162
|
+
Inject `Vary: Origin` on every response from your origin behind the CDN. Configure the CDN to respect Vary on cache lookups.
|
|
163
|
+
|
|
164
|
+
## Pattern 5 — Cap preflight cache at 24h or less
|
|
165
|
+
|
|
166
|
+
Browsers cap at 7200s anyway, so setting it to 3600 is the practical move:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Access-Control-Max-Age: 3600
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Anything higher confuses the next engineer reading the config.
|
|
173
|
+
|
|
174
|
+
## Pattern 6 — Restrict Allow-Methods
|
|
175
|
+
|
|
176
|
+
Replace `Access-Control-Allow-Methods: *` with the explicit list of methods your endpoint actually serves:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Pattern 7 — Disable Allow-Credentials if not needed
|
|
183
|
+
|
|
184
|
+
If your endpoint doesn't need to read user cookies cross-origin (most public APIs don't — they use Authorization: Bearer instead), drop `Access-Control-Allow-Credentials: true` entirely. This eliminates the entire credential-theft attack surface.
|
|
185
|
+
|
|
186
|
+
For bearer-token APIs:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Access-Control-Allow-Origin: *
|
|
190
|
+
# (no Allow-Credentials — bearer tokens come in Authorization header, not cookies)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The wildcard is safe here because the browser won't attach cookies; the only way to authenticate is the bearer token, which the cross-origin JS already had to obtain.
|
|
194
|
+
|
|
195
|
+
## CI integration
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
- name: CORS audit on changed endpoints
|
|
199
|
+
run: |
|
|
200
|
+
for ENDPOINT in $(./scripts/list-changed-endpoints.sh); do
|
|
201
|
+
python3 plugins/security/penetration-tester/skills/auditing-cors-policy/scripts/audit_cors.py \
|
|
202
|
+
"$ENDPOINT" --authorized --min-severity high --format jsonl >> cors-audit.jsonl
|
|
203
|
+
done
|
|
204
|
+
- run: |
|
|
205
|
+
if [ -s cors-audit.jsonl ] && jq -s 'any(.severity == "critical" or .severity == "high")' cors-audit.jsonl | grep -q true; then
|
|
206
|
+
echo "::error::CORS audit found high/critical findings"
|
|
207
|
+
exit 1
|
|
208
|
+
fi
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Verification after remediation
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/auditing-cors-policy/scripts/audit_cors.py \
|
|
215
|
+
https://api.example.com/endpoint \
|
|
216
|
+
--authorized \
|
|
217
|
+
--min-severity high
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Expected: exit code 0, only INFO-level finding "No CORS misconfiguration detected".
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# CORS Theory
|
|
2
|
+
|
|
3
|
+
## What CORS actually does
|
|
4
|
+
|
|
5
|
+
By default, a browser running JavaScript on `https://app.example.com`
|
|
6
|
+
cannot read responses from `https://api.different.com` — the Same-
|
|
7
|
+
Origin Policy blocks the read. CORS is the controlled relaxation: the
|
|
8
|
+
target server says "yes, this specific origin can read my responses,
|
|
9
|
+
and here are the conditions."
|
|
10
|
+
|
|
11
|
+
The browser enforces; the server announces. Two header categories:
|
|
12
|
+
|
|
13
|
+
- **Response headers** (server → browser): `Access-Control-Allow-
|
|
14
|
+
Origin`, `-Credentials`, `-Methods`, `-Headers`, `-Max-Age`, `-Expose-
|
|
15
|
+
Headers`.
|
|
16
|
+
- **Request headers** (browser → server): `Origin` (auto-set),
|
|
17
|
+
`Access-Control-Request-Method`, `-Request-Headers` (preflight only).
|
|
18
|
+
|
|
19
|
+
The browser sends `Origin` on every cross-origin request. If the
|
|
20
|
+
response doesn't allow that origin, the browser hides the response
|
|
21
|
+
from JavaScript (but the request still hit the server — caveat
|
|
22
|
+
explains the CSRF angle below).
|
|
23
|
+
|
|
24
|
+
For "non-simple" requests (anything other than GET/HEAD/POST with
|
|
25
|
+
limited content types, or with custom headers), browsers send an
|
|
26
|
+
`OPTIONS` preflight first. The server's preflight response declares
|
|
27
|
+
allowed methods + headers + cache duration. If preflight passes, the
|
|
28
|
+
real request proceeds.
|
|
29
|
+
|
|
30
|
+
## Why reflection is dangerous
|
|
31
|
+
|
|
32
|
+
The "easy" fix when a CORS bug ticket lands: read the incoming Origin
|
|
33
|
+
header and echo it into Allow-Origin. The request from
|
|
34
|
+
`https://api.partner.com` works; the request from
|
|
35
|
+
`https://my-frontend.com` works; QA closes the ticket.
|
|
36
|
+
|
|
37
|
+
The problem: requests from `https://attacker.example.com` ALSO work.
|
|
38
|
+
If the endpoint serves authenticated content and `Allow-Credentials:
|
|
39
|
+
true` is set, the browser sends the user's cookies on the cross-origin
|
|
40
|
+
request AND lets the attacker's JavaScript read the response. Full
|
|
41
|
+
session-theft via a malicious page the victim visits while logged in.
|
|
42
|
+
|
|
43
|
+
The fix is not "validate the Origin format" — it's "allow-list a
|
|
44
|
+
specific set of trusted origins server-side, check exact equality
|
|
45
|
+
against that allow-list, return the matched origin or refuse."
|
|
46
|
+
|
|
47
|
+
## The credentials + wildcard combination
|
|
48
|
+
|
|
49
|
+
The Fetch standard makes this combo illegal — browsers reject the
|
|
50
|
+
response. Server is asserting Allow-Origin:* AND Allow-Credentials:
|
|
51
|
+
true; browser sees both, refuses to expose the response to JavaScript.
|
|
52
|
+
|
|
53
|
+
So why is this a CRITICAL finding? Because the server-side intent is
|
|
54
|
+
wrong. A future server-config change could replace * with reflection,
|
|
55
|
+
or a future browser bug could relax the rule. The combination signals
|
|
56
|
+
the developer didn't understand the model — and that misunderstanding
|
|
57
|
+
will recur in the next CORS-related change.
|
|
58
|
+
|
|
59
|
+
## Subdomain-pattern bypass
|
|
60
|
+
|
|
61
|
+
A common shorthand: "trust any subdomain of example.com." The naive
|
|
62
|
+
implementation in JavaScript / config:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
if (origin.endsWith('.example.com')) { allow(origin); }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This matches `evil.example.com.attacker.com` because `.endsWith()` on
|
|
69
|
+
a string just checks the suffix — it doesn't parse the URL into host
|
|
70
|
+
parts. The attacker registers `attacker.com`, sets up DNS for
|
|
71
|
+
`*.example.com.attacker.com`, and gets a wildcard CORS pass.
|
|
72
|
+
|
|
73
|
+
The fix: parse the Origin as a URL, extract `hostname`, then check
|
|
74
|
+
either exact equality or proper subdomain logic
|
|
75
|
+
(`hostname === 'example.com' || hostname.endsWith('.example.com')`
|
|
76
|
+
— note the second form checks the leading dot, which the buggy form
|
|
77
|
+
omits).
|
|
78
|
+
|
|
79
|
+
## Origin: null
|
|
80
|
+
|
|
81
|
+
Three places this comes from:
|
|
82
|
+
|
|
83
|
+
1. Sandboxed iframes — `<iframe sandbox>` sends Origin:null.
|
|
84
|
+
2. `data:` URLs — pages loaded from data URLs send Origin:null.
|
|
85
|
+
3. `file:` URLs — local file pages send Origin:null.
|
|
86
|
+
|
|
87
|
+
If your server trusts Origin:null, any page that ends up in a
|
|
88
|
+
sandboxed iframe can read your cross-origin responses. Attackers
|
|
89
|
+
embed your target site in a sandboxed iframe on attacker.com, the
|
|
90
|
+
browser sends Origin:null, your server returns Allow-Origin:null,
|
|
91
|
+
attacker reads the response.
|
|
92
|
+
|
|
93
|
+
Never trust Origin:null. There is no legitimate cross-origin use case
|
|
94
|
+
that needs it.
|
|
95
|
+
|
|
96
|
+
## Vary:Origin and CDN cache poisoning
|
|
97
|
+
|
|
98
|
+
If your CORS response is per-origin (Allow-Origin varies based on the
|
|
99
|
+
Origin header), the cache layer must vary on Origin too. Otherwise:
|
|
100
|
+
|
|
101
|
+
1. User from `https://trusted.com` requests `/api/profile`. Server
|
|
102
|
+
responds `Allow-Origin: https://trusted.com`. CDN caches the response.
|
|
103
|
+
2. Attacker from `https://attacker.com` requests `/api/profile`. CDN
|
|
104
|
+
returns the cached response, including `Allow-Origin: https://trusted.com`.
|
|
105
|
+
3. Attacker's request still doesn't satisfy the Allow-Origin match
|
|
106
|
+
(browser checks), so no read happens — BUT the cache is now
|
|
107
|
+
serving a different origin's CORS headers to attacker, which
|
|
108
|
+
becomes a poisoning vector for downstream attacks.
|
|
109
|
+
|
|
110
|
+
The fix: `Vary: Origin` on every CORS-varying response. Every modern
|
|
111
|
+
framework's CORS middleware does this; hand-rolled CORS configs
|
|
112
|
+
forget it.
|
|
113
|
+
|
|
114
|
+
## Preflight cache duration
|
|
115
|
+
|
|
116
|
+
`Access-Control-Max-Age` controls how long the browser caches the
|
|
117
|
+
preflight response. Higher = fewer round trips (perf gain) but
|
|
118
|
+
slower CORS-policy revocation (if you change the allow-list, browsers
|
|
119
|
+
won't re-preflight until cache expires).
|
|
120
|
+
|
|
121
|
+
Chrome and Firefox cap at 7200s (2h) regardless of what the server
|
|
122
|
+
says, so values above 7200s don't actually help. Setting it explicitly
|
|
123
|
+
to 86400s (24h) or higher is a misunderstanding rather than a security
|
|
124
|
+
issue — flagged LOW for awareness.
|
|
125
|
+
|
|
126
|
+
## CORS does NOT prevent CSRF
|
|
127
|
+
|
|
128
|
+
This trips up developers. CORS controls JavaScript's ability to READ
|
|
129
|
+
responses cross-origin. The cross-origin REQUEST is still sent
|
|
130
|
+
(cookies, headers, everything). If your server changes state based on
|
|
131
|
+
the request alone (POST that updates a record without reading the
|
|
132
|
+
response), CORS doesn't help.
|
|
133
|
+
|
|
134
|
+
Pair CORS audit with CSRF audit (skill #13 in another plugin —
|
|
135
|
+
`csrf-protection-validator`) for full coverage.
|
|
136
|
+
|
|
137
|
+
## Primary sources
|
|
138
|
+
|
|
139
|
+
- [Fetch Standard — CORS protocol](https://fetch.spec.whatwg.org/#cors-protocol)
|
|
140
|
+
- [MDN — Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
141
|
+
- [OWASP A05:2021 Security Misconfiguration](https://owasp.org/Top10/A05_2021-Security_Misconfiguration/)
|
|
142
|
+
- [CWE-942 Permissive Cross-domain Policy with Untrusted Domains](https://cwe.mitre.org/data/definitions/942.html)
|