hedra 2.0.3 → 2.1.0
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.
- checksums.yaml +4 -4
- data/README.md +115 -2
- data/lib/hedra/analyzer.rb +25 -2
- data/lib/hedra/cli.rb +53 -9
- data/lib/hedra/cors_checker.rb +251 -0
- data/lib/hedra/ct_checker.rb +286 -0
- data/lib/hedra/dns_checker.rb +274 -0
- data/lib/hedra/protocol_checker.rb +228 -0
- data/lib/hedra/sri_checker.rb +151 -0
- data/lib/hedra/version.rb +1 -1
- data/lib/hedra.rb +5 -0
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b852e94cfa441a1597fb40438491f15939625b1da90734dcb0a05cb3ac06609a
|
|
4
|
+
data.tar.gz: 00bd6a8bccc0be126058ec0b8795f1b193d5e42c642041e2d5415955ccef9e81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 908b5f2e81cefb60ee58af16e89e50f092fc7338af99bd89d1b47168a8b0ef1d65044344666f46581246405c291d69fe61a1b2e930234829d48b8003df3bfa65
|
|
7
|
+
data.tar.gz: 309c0cf041eb18ab775696d8b4ca9a8771cf8c57c75709ad86f422c42ee42aebb731f620d2f573ec8edc1f338a4a7d66ee2f8078939f3c75ed33c825c847c550
|
data/README.md
CHANGED
|
@@ -150,6 +150,30 @@ hedra plugin remove plugin_name
|
|
|
150
150
|
- Signature algorithm strength
|
|
151
151
|
- Key size validation
|
|
152
152
|
- Chain verification
|
|
153
|
+
- TLS version detection (TLS 1.2/1.3)
|
|
154
|
+
- Certificate Transparency log verification
|
|
155
|
+
|
|
156
|
+
**Protocol Security:**
|
|
157
|
+
- HTTP/2 and HTTP/3 detection
|
|
158
|
+
- TLS version enforcement
|
|
159
|
+
- Insecure protocol warnings
|
|
160
|
+
|
|
161
|
+
**CORS Security:**
|
|
162
|
+
- Access-Control-Allow-Origin validation
|
|
163
|
+
- Wildcard and null origin detection
|
|
164
|
+
- Credentials with wildcard prevention
|
|
165
|
+
- Dangerous HTTP methods detection
|
|
166
|
+
- Sensitive header exposure checks
|
|
167
|
+
|
|
168
|
+
**Subresource Integrity (SRI):**
|
|
169
|
+
- External script/stylesheet SRI validation
|
|
170
|
+
- Crossorigin attribute verification
|
|
171
|
+
- Same-origin resource detection
|
|
172
|
+
|
|
173
|
+
**DNS Security:**
|
|
174
|
+
- DNSSEC validation
|
|
175
|
+
- CAA (Certificate Authority Authorization) records
|
|
176
|
+
- DNS-based security policy enforcement
|
|
153
177
|
|
|
154
178
|
**RFC 9116:**
|
|
155
179
|
- security.txt file presence and format
|
|
@@ -375,6 +399,80 @@ hedra scan -f urls.txt --output report.html --format html
|
|
|
375
399
|
|
|
376
400
|
Interactive report with sorting, filtering, and charts.
|
|
377
401
|
|
|
402
|
+
## Advanced Security Checks
|
|
403
|
+
|
|
404
|
+
### Subresource Integrity (SRI)
|
|
405
|
+
Validates that external scripts and stylesheets use SRI attributes to prevent tampering:
|
|
406
|
+
```bash
|
|
407
|
+
hedra scan https://myapp.com --check-sri
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Checks:**
|
|
411
|
+
- External resources without integrity attributes
|
|
412
|
+
- Missing crossorigin attributes
|
|
413
|
+
- Same-origin vs cross-origin detection
|
|
414
|
+
|
|
415
|
+
### CORS Policy Analysis
|
|
416
|
+
Validates Cross-Origin Resource Sharing configuration for security issues:
|
|
417
|
+
```bash
|
|
418
|
+
hedra scan https://api.myapp.com --check-cors
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Detects:**
|
|
422
|
+
- Wildcard origins with credentials (critical vulnerability)
|
|
423
|
+
- Null origin allowance
|
|
424
|
+
- Insecure HTTP origins
|
|
425
|
+
- Dangerous HTTP methods (TRACE, TRACK)
|
|
426
|
+
- Overly permissive configurations
|
|
427
|
+
- Sensitive header exposure
|
|
428
|
+
|
|
429
|
+
### Protocol Version Detection
|
|
430
|
+
Checks HTTP and TLS protocol versions:
|
|
431
|
+
```bash
|
|
432
|
+
hedra scan https://myapp.com --check-protocol
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Validates:**
|
|
436
|
+
- HTTP/1.1 vs HTTP/2 vs HTTP/3
|
|
437
|
+
- TLS 1.2/1.3 enforcement
|
|
438
|
+
- Deprecated protocol detection (SSLv3, TLS 1.0/1.1)
|
|
439
|
+
- Protocol upgrade recommendations
|
|
440
|
+
|
|
441
|
+
### Certificate Transparency
|
|
442
|
+
Verifies certificates are logged in CT logs:
|
|
443
|
+
```bash
|
|
444
|
+
hedra audit https://myapp.com --check-ct
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Checks:**
|
|
448
|
+
- SCT (Signed Certificate Timestamp) presence
|
|
449
|
+
- Multiple independent CT logs
|
|
450
|
+
- SCT delivery methods (extension, OCSP, TLS)
|
|
451
|
+
|
|
452
|
+
### DNS Security
|
|
453
|
+
Validates DNS-level security features:
|
|
454
|
+
```bash
|
|
455
|
+
hedra audit https://myapp.com --check-dns
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Validates:**
|
|
459
|
+
- DNSSEC enablement
|
|
460
|
+
- CAA records for certificate issuance control
|
|
461
|
+
- CAA tags (issue, issuewild, iodef)
|
|
462
|
+
- DNS-based security policies
|
|
463
|
+
|
|
464
|
+
### Combined Advanced Scan
|
|
465
|
+
Run all advanced checks together:
|
|
466
|
+
```bash
|
|
467
|
+
hedra audit https://myapp.com \
|
|
468
|
+
--check-sri \
|
|
469
|
+
--check-cors \
|
|
470
|
+
--check-protocol \
|
|
471
|
+
--check-ct \
|
|
472
|
+
--check-dns \
|
|
473
|
+
--output comprehensive-report.json
|
|
474
|
+
```
|
|
475
|
+
|
|
378
476
|
## Real-World Examples
|
|
379
477
|
|
|
380
478
|
### Basic Security Audit
|
|
@@ -382,6 +480,23 @@ Interactive report with sorting, filtering, and charts.
|
|
|
382
480
|
hedra scan https://myapp.com
|
|
383
481
|
```
|
|
384
482
|
|
|
483
|
+
### Comprehensive Security Audit with All Checks
|
|
484
|
+
```bash
|
|
485
|
+
hedra audit https://myapp.com \
|
|
486
|
+
--check-sri \
|
|
487
|
+
--check-ct \
|
|
488
|
+
--check-dns \
|
|
489
|
+
--json \
|
|
490
|
+
--output full-audit.json
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Quick CORS and Protocol Check
|
|
494
|
+
```bash
|
|
495
|
+
hedra scan https://api.myapp.com \
|
|
496
|
+
--check-cors \
|
|
497
|
+
--check-protocol
|
|
498
|
+
```
|
|
499
|
+
|
|
385
500
|
### Production Deployment Check
|
|
386
501
|
```bash
|
|
387
502
|
# Save baseline after deployment
|
|
@@ -503,5 +618,3 @@ hedra scan https://slow-server.com --timeout 60
|
|
|
503
618
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
504
619
|
|
|
505
620
|
---
|
|
506
|
-
|
|
507
|
-
**Built by [BlackStack](https://github.com/bl4ckstack)** • Securing the web, one header at a time.
|
data/lib/hedra/analyzer.rb
CHANGED
|
@@ -61,17 +61,22 @@ module Hedra
|
|
|
61
61
|
}
|
|
62
62
|
}.freeze
|
|
63
63
|
|
|
64
|
-
def initialize(check_certificates: true, check_security_txt: false)
|
|
64
|
+
def initialize(check_certificates: true, check_security_txt: false, check_sri: false, check_cors: true, check_protocol: true, check_ct: false, check_dns: false)
|
|
65
65
|
@plugin_manager = PluginManager.new
|
|
66
66
|
@scorer = Scorer.new
|
|
67
67
|
@certificate_checker = check_certificates ? CertificateChecker.new : nil
|
|
68
68
|
@security_txt_checker = check_security_txt ? SecurityTxtChecker.new : nil
|
|
69
|
+
@sri_checker = check_sri ? SriChecker.new : nil
|
|
70
|
+
@cors_checker = check_cors ? CorsChecker.new : nil
|
|
71
|
+
@protocol_checker = check_protocol ? ProtocolChecker.new : nil
|
|
72
|
+
@ct_checker = check_ct ? CtChecker.new : nil
|
|
73
|
+
@dns_checker = check_dns ? DnsChecker.new : nil
|
|
69
74
|
@custom_rules = []
|
|
70
75
|
@mutex = Mutex.new # Thread safety for custom rules
|
|
71
76
|
load_custom_rules
|
|
72
77
|
end
|
|
73
78
|
|
|
74
|
-
def analyze(url, headers, http_client: nil)
|
|
79
|
+
def analyze(url, headers, http_client: nil, response: nil, html_content: nil)
|
|
75
80
|
normalized_headers = normalize_headers(headers)
|
|
76
81
|
findings = []
|
|
77
82
|
|
|
@@ -104,6 +109,24 @@ module Hedra
|
|
|
104
109
|
# Check security.txt
|
|
105
110
|
findings.concat(@security_txt_checker.check(url, http_client)) if @security_txt_checker && http_client
|
|
106
111
|
|
|
112
|
+
# Check Subresource Integrity (SRI)
|
|
113
|
+
if @sri_checker
|
|
114
|
+
@sri_checker.instance_variable_set(:@http_client, http_client) if http_client
|
|
115
|
+
findings.concat(@sri_checker.check(url, html_content))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check CORS configuration
|
|
119
|
+
findings.concat(@cors_checker.check(normalized_headers, url)) if @cors_checker
|
|
120
|
+
|
|
121
|
+
# Check HTTP/TLS protocol versions
|
|
122
|
+
findings.concat(@protocol_checker.check(url, response)) if @protocol_checker
|
|
123
|
+
|
|
124
|
+
# Check Certificate Transparency
|
|
125
|
+
findings.concat(@ct_checker.check(url)) if @ct_checker
|
|
126
|
+
|
|
127
|
+
# Check DNS security (DNSSEC, CAA)
|
|
128
|
+
findings.concat(@dns_checker.check(url)) if @dns_checker
|
|
129
|
+
|
|
107
130
|
# Calculate security score
|
|
108
131
|
score = @scorer.calculate(normalized_headers, findings)
|
|
109
132
|
|
data/lib/hedra/cli.rb
CHANGED
|
@@ -71,7 +71,8 @@ module Hedra
|
|
|
71
71
|
|
|
72
72
|
urls.each do |url|
|
|
73
73
|
response = client.get(url)
|
|
74
|
-
|
|
74
|
+
headers = safe_headers_to_hash(response.headers)
|
|
75
|
+
result = analyzer.analyze(url, headers)
|
|
75
76
|
current_results << result
|
|
76
77
|
rescue StandardError => e
|
|
77
78
|
warn "Failed to scan #{url}: #{e.message}"
|
|
@@ -177,6 +178,11 @@ module Hedra
|
|
|
177
178
|
option :cache_ttl, type: :numeric, default: 3600, desc: 'Cache TTL in seconds'
|
|
178
179
|
option :check_certificates, type: :boolean, default: true, desc: 'Check SSL certificates'
|
|
179
180
|
option :check_security_txt, type: :boolean, default: false, desc: 'Check for security.txt'
|
|
181
|
+
option :check_sri, type: :boolean, default: false, desc: 'Check Subresource Integrity'
|
|
182
|
+
option :check_cors, type: :boolean, default: true, desc: 'Check CORS configuration'
|
|
183
|
+
option :check_protocol, type: :boolean, default: true, desc: 'Check HTTP/TLS protocol versions'
|
|
184
|
+
option :check_ct, type: :boolean, default: false, desc: 'Check Certificate Transparency'
|
|
185
|
+
option :check_dns, type: :boolean, default: false, desc: 'Check DNSSEC and CAA records'
|
|
180
186
|
option :save_baseline, type: :string, desc: 'Save results as baseline'
|
|
181
187
|
option :progress, type: :boolean, default: true, desc: 'Show progress bar'
|
|
182
188
|
def scan(target) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -186,7 +192,12 @@ module Hedra
|
|
|
186
192
|
client = build_http_client
|
|
187
193
|
analyzer = Analyzer.new(
|
|
188
194
|
check_certificates: options[:check_certificates],
|
|
189
|
-
check_security_txt: options[:check_security_txt]
|
|
195
|
+
check_security_txt: options[:check_security_txt],
|
|
196
|
+
check_sri: options[:check_sri],
|
|
197
|
+
check_cors: options[:check_cors],
|
|
198
|
+
check_protocol: options[:check_protocol],
|
|
199
|
+
check_ct: options[:check_ct],
|
|
200
|
+
check_dns: options[:check_dns]
|
|
190
201
|
)
|
|
191
202
|
cache = options[:cache] ? Cache.new(ttl: options[:cache_ttl]) : nil
|
|
192
203
|
rate_limiter = options[:rate] ? RateLimiter.new(options[:rate]) : nil
|
|
@@ -211,7 +222,9 @@ module Hedra
|
|
|
211
222
|
log_info("Cache hit: #{url}") if @verbose
|
|
212
223
|
else
|
|
213
224
|
response = client.get(url)
|
|
214
|
-
|
|
225
|
+
headers = safe_headers_to_hash(response.headers)
|
|
226
|
+
html_content = response.body.to_s if options[:check_sri]
|
|
227
|
+
result = analyzer.analyze(url, headers, http_client: client, response: response, html_content: html_content)
|
|
215
228
|
cache&.set(url, result)
|
|
216
229
|
end
|
|
217
230
|
|
|
@@ -246,17 +259,29 @@ module Hedra
|
|
|
246
259
|
option :timeout, type: :numeric, aliases: '-t', default: 10, desc: 'Request timeout'
|
|
247
260
|
option :check_certificates, type: :boolean, default: true, desc: 'Check SSL certificates'
|
|
248
261
|
option :check_security_txt, type: :boolean, default: true, desc: 'Check for security.txt'
|
|
262
|
+
option :check_sri, type: :boolean, default: true, desc: 'Check Subresource Integrity'
|
|
263
|
+
option :check_cors, type: :boolean, default: true, desc: 'Check CORS configuration'
|
|
264
|
+
option :check_protocol, type: :boolean, default: true, desc: 'Check HTTP/TLS protocol versions'
|
|
265
|
+
option :check_ct, type: :boolean, default: true, desc: 'Check Certificate Transparency'
|
|
266
|
+
option :check_dns, type: :boolean, default: true, desc: 'Check DNSSEC and CAA records'
|
|
249
267
|
def audit(url)
|
|
250
268
|
setup_logging
|
|
251
269
|
client = build_http_client
|
|
252
270
|
analyzer = Analyzer.new(
|
|
253
271
|
check_certificates: options[:check_certificates],
|
|
254
|
-
check_security_txt: options[:check_security_txt]
|
|
272
|
+
check_security_txt: options[:check_security_txt],
|
|
273
|
+
check_sri: options[:check_sri],
|
|
274
|
+
check_cors: options[:check_cors],
|
|
275
|
+
check_protocol: options[:check_protocol],
|
|
276
|
+
check_ct: options[:check_ct],
|
|
277
|
+
check_dns: options[:check_dns]
|
|
255
278
|
)
|
|
256
279
|
|
|
257
280
|
begin
|
|
258
281
|
response = client.get(url)
|
|
259
|
-
|
|
282
|
+
headers = safe_headers_to_hash(response.headers)
|
|
283
|
+
html_content = response.body.to_s if options[:check_sri]
|
|
284
|
+
result = analyzer.analyze(url, headers, http_client: client, response: response, html_content: html_content)
|
|
260
285
|
|
|
261
286
|
if options[:json]
|
|
262
287
|
output = JSON.pretty_generate(result)
|
|
@@ -289,7 +314,8 @@ module Hedra
|
|
|
289
314
|
loop do
|
|
290
315
|
begin
|
|
291
316
|
response = client.get(url)
|
|
292
|
-
|
|
317
|
+
headers = safe_headers_to_hash(response.headers)
|
|
318
|
+
result = analyzer.analyze(url, headers)
|
|
293
319
|
print_result(result)
|
|
294
320
|
rescue StandardError => e
|
|
295
321
|
log_error("Watch check failed: #{e.message}")
|
|
@@ -311,8 +337,10 @@ module Hedra
|
|
|
311
337
|
response1 = client.get(url1)
|
|
312
338
|
response2 = client.get(url2)
|
|
313
339
|
|
|
314
|
-
|
|
315
|
-
|
|
340
|
+
headers1 = safe_headers_to_hash(response1.headers)
|
|
341
|
+
headers2 = safe_headers_to_hash(response2.headers)
|
|
342
|
+
result1 = analyzer.analyze(url1, headers1)
|
|
343
|
+
result2 = analyzer.analyze(url2, headers2)
|
|
316
344
|
|
|
317
345
|
print_comparison(result1, result2)
|
|
318
346
|
rescue StandardError => e
|
|
@@ -367,7 +395,8 @@ module Hedra
|
|
|
367
395
|
|
|
368
396
|
urls.each do |url|
|
|
369
397
|
response = client.get(url)
|
|
370
|
-
|
|
398
|
+
headers = safe_headers_to_hash(response.headers)
|
|
399
|
+
result = analyzer.analyze(url, headers, http_client: client)
|
|
371
400
|
results << result
|
|
372
401
|
|
|
373
402
|
if result[:score] < options[:threshold]
|
|
@@ -558,6 +587,21 @@ module Hedra
|
|
|
558
587
|
puts Pastel.new.cyan("INFO: #{message}")
|
|
559
588
|
end
|
|
560
589
|
|
|
590
|
+
def safe_headers_to_hash(headers)
|
|
591
|
+
return {} if headers.nil?
|
|
592
|
+
|
|
593
|
+
# Handle different header object types
|
|
594
|
+
hash = headers.respond_to?(:to_h) ? headers.to_h : {}
|
|
595
|
+
|
|
596
|
+
# Clean up nil values
|
|
597
|
+
hash.compact.transform_values do |value|
|
|
598
|
+
value.nil? ? '' : value
|
|
599
|
+
end
|
|
600
|
+
rescue StandardError => e
|
|
601
|
+
log_error("Failed to convert headers: #{e.message}") if @debug
|
|
602
|
+
{}
|
|
603
|
+
end
|
|
604
|
+
|
|
561
605
|
def severity_badge(severity)
|
|
562
606
|
pastel = Pastel.new
|
|
563
607
|
case severity.to_s
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hedra
|
|
4
|
+
# Validate CORS (Cross-Origin Resource Sharing) headers for security misconfigurations
|
|
5
|
+
class CorsChecker
|
|
6
|
+
DANGEROUS_ORIGINS = [
|
|
7
|
+
'*',
|
|
8
|
+
'null'
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
def check(headers, url = nil)
|
|
12
|
+
findings = []
|
|
13
|
+
|
|
14
|
+
# Check Access-Control-Allow-Origin
|
|
15
|
+
if headers.key?('access-control-allow-origin')
|
|
16
|
+
acao = headers['access-control-allow-origin']
|
|
17
|
+
findings.concat(check_allow_origin(acao, headers))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check Access-Control-Allow-Credentials with wildcard
|
|
21
|
+
if headers.key?('access-control-allow-credentials')
|
|
22
|
+
findings.concat(check_credentials(headers))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check Access-Control-Allow-Methods
|
|
26
|
+
if headers.key?('access-control-allow-methods')
|
|
27
|
+
findings.concat(check_allow_methods(headers['access-control-allow-methods']))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check Access-Control-Allow-Headers
|
|
31
|
+
if headers.key?('access-control-allow-headers')
|
|
32
|
+
findings.concat(check_allow_headers(headers['access-control-allow-headers']))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check Access-Control-Max-Age
|
|
36
|
+
if headers.key?('access-control-max-age')
|
|
37
|
+
findings.concat(check_max_age(headers['access-control-max-age']))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check for missing CORS headers when others are present
|
|
41
|
+
if cors_headers_present?(headers) && !headers.key?('access-control-allow-origin')
|
|
42
|
+
findings << {
|
|
43
|
+
header: 'access-control-allow-origin',
|
|
44
|
+
issue: 'CORS headers present but Access-Control-Allow-Origin is missing',
|
|
45
|
+
severity: :warning,
|
|
46
|
+
recommended_fix: 'Add Access-Control-Allow-Origin header or remove other CORS headers'
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
findings
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
warn "CORS check failed: #{e.message}" if ENV['DEBUG']
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def check_allow_origin(acao, headers)
|
|
59
|
+
findings = []
|
|
60
|
+
|
|
61
|
+
# Wildcard with credentials is a critical security issue
|
|
62
|
+
if acao == '*' && credentials_enabled?(headers)
|
|
63
|
+
findings << {
|
|
64
|
+
header: 'access-control-allow-origin',
|
|
65
|
+
issue: 'CORS allows all origins (*) with credentials enabled - critical security risk',
|
|
66
|
+
severity: :critical,
|
|
67
|
+
recommended_fix: 'Use specific origin instead of wildcard when credentials are enabled'
|
|
68
|
+
}
|
|
69
|
+
elsif acao == '*'
|
|
70
|
+
findings << {
|
|
71
|
+
header: 'access-control-allow-origin',
|
|
72
|
+
issue: 'CORS allows all origins (*) - potential security risk',
|
|
73
|
+
severity: :warning,
|
|
74
|
+
recommended_fix: 'Restrict to specific trusted origins'
|
|
75
|
+
}
|
|
76
|
+
elsif acao == 'null'
|
|
77
|
+
findings << {
|
|
78
|
+
header: 'access-control-allow-origin',
|
|
79
|
+
issue: 'CORS allows "null" origin - security vulnerability',
|
|
80
|
+
severity: :critical,
|
|
81
|
+
recommended_fix: 'Never allow "null" origin, use specific origins'
|
|
82
|
+
}
|
|
83
|
+
elsif acao.include?(',')
|
|
84
|
+
findings << {
|
|
85
|
+
header: 'access-control-allow-origin',
|
|
86
|
+
issue: 'Multiple origins in Access-Control-Allow-Origin (invalid syntax)',
|
|
87
|
+
severity: :critical,
|
|
88
|
+
recommended_fix: 'Use single origin or implement dynamic origin validation'
|
|
89
|
+
}
|
|
90
|
+
elsif acao.match?(%r{^https?://})
|
|
91
|
+
# Valid origin, check for common issues
|
|
92
|
+
if acao.start_with?('http://') && !acao.include?('localhost') && !acao.include?('127.0.0.1')
|
|
93
|
+
findings << {
|
|
94
|
+
header: 'access-control-allow-origin',
|
|
95
|
+
issue: 'CORS allows insecure HTTP origin',
|
|
96
|
+
severity: :warning,
|
|
97
|
+
recommended_fix: 'Use HTTPS origins only'
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
findings
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def check_credentials(headers)
|
|
106
|
+
findings = []
|
|
107
|
+
acac = headers['access-control-allow-credentials']
|
|
108
|
+
|
|
109
|
+
if acac.to_s.downcase == 'true'
|
|
110
|
+
acao = headers['access-control-allow-origin']
|
|
111
|
+
|
|
112
|
+
if acao == '*'
|
|
113
|
+
findings << {
|
|
114
|
+
header: 'access-control-allow-credentials',
|
|
115
|
+
issue: 'Credentials enabled with wildcard origin (invalid and dangerous)',
|
|
116
|
+
severity: :critical,
|
|
117
|
+
recommended_fix: 'Use specific origin when credentials are enabled'
|
|
118
|
+
}
|
|
119
|
+
elsif acao.nil?
|
|
120
|
+
findings << {
|
|
121
|
+
header: 'access-control-allow-credentials',
|
|
122
|
+
issue: 'Credentials enabled without Access-Control-Allow-Origin',
|
|
123
|
+
severity: :warning,
|
|
124
|
+
recommended_fix: 'Add Access-Control-Allow-Origin header'
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
elsif acac && acac.to_s.downcase != 'true'
|
|
128
|
+
findings << {
|
|
129
|
+
header: 'access-control-allow-credentials',
|
|
130
|
+
issue: 'Invalid Access-Control-Allow-Credentials value (must be "true" or omitted)',
|
|
131
|
+
severity: :warning,
|
|
132
|
+
recommended_fix: 'Set to "true" or remove the header'
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
findings
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def check_allow_methods(methods)
|
|
140
|
+
findings = []
|
|
141
|
+
method_list = methods.to_s.upcase.split(',').map(&:strip)
|
|
142
|
+
|
|
143
|
+
# Check for overly permissive methods
|
|
144
|
+
dangerous_methods = ['TRACE', 'TRACK', 'CONNECT']
|
|
145
|
+
found_dangerous = method_list & dangerous_methods
|
|
146
|
+
|
|
147
|
+
if found_dangerous.any?
|
|
148
|
+
findings << {
|
|
149
|
+
header: 'access-control-allow-methods',
|
|
150
|
+
issue: "Dangerous HTTP methods allowed: #{found_dangerous.join(', ')}",
|
|
151
|
+
severity: :critical,
|
|
152
|
+
recommended_fix: 'Remove dangerous methods (TRACE, TRACK, CONNECT)'
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check for wildcard
|
|
157
|
+
if method_list.include?('*')
|
|
158
|
+
findings << {
|
|
159
|
+
header: 'access-control-allow-methods',
|
|
160
|
+
issue: 'CORS allows all HTTP methods (*)',
|
|
161
|
+
severity: :warning,
|
|
162
|
+
recommended_fix: 'Specify only required methods (GET, POST, etc.)'
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check for overly permissive DELETE/PUT without proper consideration
|
|
167
|
+
risky_methods = ['DELETE', 'PUT', 'PATCH']
|
|
168
|
+
found_risky = method_list & risky_methods
|
|
169
|
+
|
|
170
|
+
if found_risky.any? && method_list.length > 5
|
|
171
|
+
findings << {
|
|
172
|
+
header: 'access-control-allow-methods',
|
|
173
|
+
issue: "Potentially risky methods allowed: #{found_risky.join(', ')}",
|
|
174
|
+
severity: :info,
|
|
175
|
+
recommended_fix: 'Ensure write methods are properly protected with authentication'
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
findings
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def check_allow_headers(headers_value)
|
|
183
|
+
findings = []
|
|
184
|
+
header_list = headers_value.to_s.downcase.split(',').map(&:strip)
|
|
185
|
+
|
|
186
|
+
# Check for wildcard
|
|
187
|
+
if header_list.include?('*')
|
|
188
|
+
findings << {
|
|
189
|
+
header: 'access-control-allow-headers',
|
|
190
|
+
issue: 'CORS allows all headers (*)',
|
|
191
|
+
severity: :warning,
|
|
192
|
+
recommended_fix: 'Specify only required headers'
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check for sensitive headers
|
|
197
|
+
sensitive_headers = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']
|
|
198
|
+
found_sensitive = header_list & sensitive_headers
|
|
199
|
+
|
|
200
|
+
if found_sensitive.any?
|
|
201
|
+
findings << {
|
|
202
|
+
header: 'access-control-allow-headers',
|
|
203
|
+
issue: "Sensitive headers exposed: #{found_sensitive.join(', ')}",
|
|
204
|
+
severity: :info,
|
|
205
|
+
recommended_fix: 'Ensure sensitive headers are only allowed for trusted origins'
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
findings
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def check_max_age(max_age)
|
|
213
|
+
findings = []
|
|
214
|
+
age = max_age.to_i
|
|
215
|
+
|
|
216
|
+
if age <= 0
|
|
217
|
+
findings << {
|
|
218
|
+
header: 'access-control-max-age',
|
|
219
|
+
issue: 'Invalid Access-Control-Max-Age value',
|
|
220
|
+
severity: :info,
|
|
221
|
+
recommended_fix: 'Set to positive number of seconds (e.g., 3600)'
|
|
222
|
+
}
|
|
223
|
+
elsif age > 86400
|
|
224
|
+
findings << {
|
|
225
|
+
header: 'access-control-max-age',
|
|
226
|
+
issue: 'Very long preflight cache duration (>24 hours)',
|
|
227
|
+
severity: :info,
|
|
228
|
+
recommended_fix: 'Consider shorter duration for security updates'
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
findings
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def credentials_enabled?(headers)
|
|
236
|
+
headers['access-control-allow-credentials'].to_s.downcase == 'true'
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def cors_headers_present?(headers)
|
|
240
|
+
cors_header_keys = [
|
|
241
|
+
'access-control-allow-credentials',
|
|
242
|
+
'access-control-allow-methods',
|
|
243
|
+
'access-control-allow-headers',
|
|
244
|
+
'access-control-max-age',
|
|
245
|
+
'access-control-expose-headers'
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
cors_header_keys.any? { |key| headers.key?(key) }
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|