Skip to content

Commit bd91f7f

Browse files
authored
feat(frameworks): add web application firewall detectors (#360)
detect cloudflare waf, sucuri, incapsula, f5 big-ip asm and modsecurity from the headers, cookies and block-page strings the waf itself sets, so a site that merely names the vendor in prose does not trip them. grouped in one file so the category can be reviewed or excluded as a unit.
1 parent 059d735 commit bd91f7f

2 files changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
/*
14+
15+
BSD 3-Clause License
16+
(c) 2022-2026 vmfunc, xyzeva & contributors
17+
18+
*/
19+
20+
package detectors
21+
22+
import (
23+
"net/http"
24+
25+
fw "github.com/vmfunc/sif/internal/scan/frameworks"
26+
)
27+
28+
// The detectors here identify the WAF in front of a target. Each keys on a
29+
// header, Set-Cookie name, or block-page string the WAF itself sets, so a page
30+
// that merely names the vendor in prose does not trip it.
31+
func init() {
32+
fw.Register(&cloudflareWAFDetector{})
33+
fw.Register(&sucuriDetector{})
34+
fw.Register(&incapsulaDetector{})
35+
fw.Register(&bigipASMDetector{})
36+
fw.Register(&modSecurityDetector{})
37+
}
38+
39+
// cloudflareWAFDetector detects an active Cloudflare WAF or bot challenge via the
40+
// cf-mitigated header and __cf_chl_ tokens, which appear only on an interception
41+
// response (plain proxying is covered by the cf-ray CDN marker).
42+
type cloudflareWAFDetector struct{}
43+
44+
func (d *cloudflareWAFDetector) Name() string { return "Cloudflare WAF" }
45+
46+
func (d *cloudflareWAFDetector) Signatures() []fw.Signature {
47+
return []fw.Signature{
48+
{Pattern: "cf-mitigated", Weight: 0.6, HeaderOnly: true},
49+
{Pattern: "__cf_chl_", Weight: 0.5},
50+
{Pattern: "Attention Required! | Cloudflare", Weight: 0.4},
51+
}
52+
}
53+
54+
func (d *cloudflareWAFDetector) Detect(body string, headers http.Header) (float32, string) {
55+
base := fw.NewBaseDetector(d.Name(), d.Signatures())
56+
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
57+
}
58+
59+
// sucuriDetector detects the Sucuri (GoDaddy) cloud WAF via its x-sucuri-id and
60+
// x-sucuri-cache headers, backed by the block-page string.
61+
type sucuriDetector struct{}
62+
63+
func (d *sucuriDetector) Name() string { return "Sucuri WAF" }
64+
65+
func (d *sucuriDetector) Signatures() []fw.Signature {
66+
return []fw.Signature{
67+
{Pattern: "x-sucuri-id", Weight: 0.6, HeaderOnly: true},
68+
{Pattern: "x-sucuri-cache", Weight: 0.4, HeaderOnly: true},
69+
{Pattern: "Sucuri WebSite Firewall", Weight: 0.5},
70+
}
71+
}
72+
73+
func (d *sucuriDetector) Detect(body string, headers http.Header) (float32, string) {
74+
base := fw.NewBaseDetector(d.Name(), d.Signatures())
75+
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
76+
}
77+
78+
// incapsulaDetector detects the Imperva Incapsula cloud WAF via its x-iinfo
79+
// header, incap_ses_ Set-Cookie name, and block-page incident string. Each is
80+
// Incapsula-exclusive and clears the threshold on its own.
81+
type incapsulaDetector struct{}
82+
83+
func (d *incapsulaDetector) Name() string { return "Imperva Incapsula" }
84+
85+
func (d *incapsulaDetector) Signatures() []fw.Signature {
86+
return []fw.Signature{
87+
{Pattern: "x-iinfo", Weight: 0.6, HeaderOnly: true},
88+
{Pattern: "incap_ses_", Weight: 0.5, HeaderOnly: true},
89+
{Pattern: "Incapsula incident ID", Weight: 0.5},
90+
}
91+
}
92+
93+
func (d *incapsulaDetector) Detect(body string, headers http.Header) (float32, string) {
94+
base := fw.NewBaseDetector(d.Name(), d.Signatures())
95+
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
96+
}
97+
98+
// bigipASMDetector detects the F5 BIG-IP Application Security Manager via its
99+
// block-page rejection sentence and support-id line. Block-page only, so it never
100+
// fires on a passing response.
101+
type bigipASMDetector struct{}
102+
103+
func (d *bigipASMDetector) Name() string { return "F5 BIG-IP ASM" }
104+
105+
func (d *bigipASMDetector) Signatures() []fw.Signature {
106+
return []fw.Signature{
107+
{Pattern: "The requested URL was rejected. Please consult with your administrator.", Weight: 0.6},
108+
{Pattern: "Your support ID is:", Weight: 0.3},
109+
}
110+
}
111+
112+
func (d *bigipASMDetector) Detect(body string, headers http.Header) (float32, string) {
113+
base := fw.NewBaseDetector(d.Name(), d.Signatures())
114+
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
115+
}
116+
117+
// modSecurityDetector detects ModSecurity via its generated block page (primary)
118+
// and the Mod_Security/NOYB server-token values (supporting, since suppressible).
119+
type modSecurityDetector struct{}
120+
121+
func (d *modSecurityDetector) Name() string { return "ModSecurity" }
122+
123+
func (d *modSecurityDetector) Signatures() []fw.Signature {
124+
return []fw.Signature{
125+
{Pattern: "This error was generated by Mod_Security", Weight: 0.6},
126+
{Pattern: "Mod_Security", Weight: 0.3, HeaderOnly: true},
127+
{Pattern: "NOYB", Weight: 0.2, HeaderOnly: true},
128+
}
129+
}
130+
131+
func (d *modSecurityDetector) Detect(body string, headers http.Header) (float32, string) {
132+
base := fw.NewBaseDetector(d.Name(), d.Signatures())
133+
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
134+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package detectors
14+
15+
import (
16+
"net/http"
17+
"testing"
18+
19+
fw "github.com/vmfunc/sif/internal/scan/frameworks"
20+
)
21+
22+
// TestWAFDetectors_Positive fires each WAF detector on a marker shaped like the
23+
// real header, Set-Cookie, or block-page body that WAF emits.
24+
func TestWAFDetectors_Positive(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
detector fw.Detector
28+
body string
29+
headers http.Header
30+
}{
31+
{"Cloudflare mitigated header", &cloudflareWAFDetector{}, "", http.Header{"Cf-Mitigated": {"challenge"}}},
32+
{"Cloudflare challenge page", &cloudflareWAFDetector{}, `<title>Attention Required! | Cloudflare</title><script>window.__cf_chl_opt={};</script>`, http.Header{}},
33+
{"Sucuri id header", &sucuriDetector{}, "", http.Header{"X-Sucuri-Id": {"17.5"}}},
34+
{"Sucuri block page", &sucuriDetector{}, `<title>Sucuri WebSite Firewall - Access Denied</title>`, http.Header{"X-Sucuri-Cache": {"MISS"}}},
35+
{"Incapsula iinfo header", &incapsulaDetector{}, "", http.Header{"X-Iinfo": {"9-12345-0 0NNN RT(1) q(0) r(0)"}}},
36+
{"Incapsula cookies", &incapsulaDetector{}, "", http.Header{"Set-Cookie": {"visid_incap_123=abc; path=/", "incap_ses_456=def; path=/"}}},
37+
{"Incapsula incident page", &incapsulaDetector{}, `Request unsuccessful. Incapsula incident ID: 123-456`, http.Header{}},
38+
{"F5 BIG-IP ASM block", &bigipASMDetector{}, `<html><body>The requested URL was rejected. Please consult with your administrator.<br>Your support ID is: 987654321</body></html>`, http.Header{}},
39+
{"ModSecurity block page", &modSecurityDetector{}, `<p>This error was generated by Mod_Security.</p>`, http.Header{}},
40+
{"ModSecurity server token", &modSecurityDetector{}, `This error was generated by Mod_Security.`, http.Header{"Server": {"Apache Mod_Security"}}},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
conf, _ := tt.detector.Detect(tt.body, tt.headers)
46+
if conf <= 0.5 {
47+
t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf)
48+
}
49+
})
50+
}
51+
}
52+
53+
// TestWAFDetectors_Negative keeps the detectors quiet on prose that names the
54+
// vendor and on unrelated proxy headers, so a mention or a coincidental header
55+
// cannot trip a WAF finding.
56+
func TestWAFDetectors_Negative(t *testing.T) {
57+
tests := []struct {
58+
name string
59+
detector fw.Detector
60+
body string
61+
headers http.Header
62+
}{
63+
{"Cloudflare plain proxy", &cloudflareWAFDetector{}, "", http.Header{"Cf-Ray": {"8a1-EWR"}}},
64+
{"Cloudflare prose", &cloudflareWAFDetector{}, `<p>We compared Cloudflare and Fastly for WAF coverage.</p>`, http.Header{}},
65+
{"Sucuri prose", &sucuriDetector{}, `<p>Sucuri is a popular website firewall.</p>`, http.Header{}},
66+
{"Incapsula prose", &incapsulaDetector{}, `<p>Imperva Incapsula competes with Cloudflare.</p>`, http.Header{}},
67+
{"F5 unrelated body", &bigipASMDetector{}, `<p>Our support team can help with your F5 BIG-IP setup.</p>`, http.Header{}},
68+
{"ModSecurity prose", &modSecurityDetector{}, `<p>We enabled ModSecurity rules on the origin.</p>`, http.Header{}},
69+
{"plain nginx", &sucuriDetector{}, "", http.Header{"Server": {"nginx/1.25.3"}}},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
conf, _ := tt.detector.Detect(tt.body, tt.headers)
75+
if conf > 0.5 {
76+
t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf)
77+
}
78+
})
79+
}
80+
}

0 commit comments

Comments
 (0)