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.
This commit is contained in:
Tigah
2026-07-22 12:57:18 -07:00
committed by GitHub
parent 059d7356bf
commit bd91f7f590
2 changed files with 214 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package detectors
import (
"net/http"
fw "github.com/vmfunc/sif/internal/scan/frameworks"
)
// The detectors here identify the WAF in front of a target. Each keys on a
// header, Set-Cookie name, or block-page string the WAF itself sets, so a page
// that merely names the vendor in prose does not trip it.
func init() {
fw.Register(&cloudflareWAFDetector{})
fw.Register(&sucuriDetector{})
fw.Register(&incapsulaDetector{})
fw.Register(&bigipASMDetector{})
fw.Register(&modSecurityDetector{})
}
// cloudflareWAFDetector detects an active Cloudflare WAF or bot challenge via the
// cf-mitigated header and __cf_chl_ tokens, which appear only on an interception
// response (plain proxying is covered by the cf-ray CDN marker).
type cloudflareWAFDetector struct{}
func (d *cloudflareWAFDetector) Name() string { return "Cloudflare WAF" }
func (d *cloudflareWAFDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "cf-mitigated", Weight: 0.6, HeaderOnly: true},
{Pattern: "__cf_chl_", Weight: 0.5},
{Pattern: "Attention Required! | Cloudflare", Weight: 0.4},
}
}
func (d *cloudflareWAFDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}
// sucuriDetector detects the Sucuri (GoDaddy) cloud WAF via its x-sucuri-id and
// x-sucuri-cache headers, backed by the block-page string.
type sucuriDetector struct{}
func (d *sucuriDetector) Name() string { return "Sucuri WAF" }
func (d *sucuriDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "x-sucuri-id", Weight: 0.6, HeaderOnly: true},
{Pattern: "x-sucuri-cache", Weight: 0.4, HeaderOnly: true},
{Pattern: "Sucuri WebSite Firewall", Weight: 0.5},
}
}
func (d *sucuriDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}
// incapsulaDetector detects the Imperva Incapsula cloud WAF via its x-iinfo
// header, incap_ses_ Set-Cookie name, and block-page incident string. Each is
// Incapsula-exclusive and clears the threshold on its own.
type incapsulaDetector struct{}
func (d *incapsulaDetector) Name() string { return "Imperva Incapsula" }
func (d *incapsulaDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "x-iinfo", Weight: 0.6, HeaderOnly: true},
{Pattern: "incap_ses_", Weight: 0.5, HeaderOnly: true},
{Pattern: "Incapsula incident ID", Weight: 0.5},
}
}
func (d *incapsulaDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}
// bigipASMDetector detects the F5 BIG-IP Application Security Manager via its
// block-page rejection sentence and support-id line. Block-page only, so it never
// fires on a passing response.
type bigipASMDetector struct{}
func (d *bigipASMDetector) Name() string { return "F5 BIG-IP ASM" }
func (d *bigipASMDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "The requested URL was rejected. Please consult with your administrator.", Weight: 0.6},
{Pattern: "Your support ID is:", Weight: 0.3},
}
}
func (d *bigipASMDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}
// modSecurityDetector detects ModSecurity via its generated block page (primary)
// and the Mod_Security/NOYB server-token values (supporting, since suppressible).
type modSecurityDetector struct{}
func (d *modSecurityDetector) Name() string { return "ModSecurity" }
func (d *modSecurityDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "This error was generated by Mod_Security", Weight: 0.6},
{Pattern: "Mod_Security", Weight: 0.3, HeaderOnly: true},
{Pattern: "NOYB", Weight: 0.2, HeaderOnly: true},
}
}
func (d *modSecurityDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}
@@ -0,0 +1,80 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package detectors
import (
"net/http"
"testing"
fw "github.com/vmfunc/sif/internal/scan/frameworks"
)
// TestWAFDetectors_Positive fires each WAF detector on a marker shaped like the
// real header, Set-Cookie, or block-page body that WAF emits.
func TestWAFDetectors_Positive(t *testing.T) {
tests := []struct {
name string
detector fw.Detector
body string
headers http.Header
}{
{"Cloudflare mitigated header", &cloudflareWAFDetector{}, "", http.Header{"Cf-Mitigated": {"challenge"}}},
{"Cloudflare challenge page", &cloudflareWAFDetector{}, `<title>Attention Required! | Cloudflare</title><script>window.__cf_chl_opt={};</script>`, http.Header{}},
{"Sucuri id header", &sucuriDetector{}, "", http.Header{"X-Sucuri-Id": {"17.5"}}},
{"Sucuri block page", &sucuriDetector{}, `<title>Sucuri WebSite Firewall - Access Denied</title>`, http.Header{"X-Sucuri-Cache": {"MISS"}}},
{"Incapsula iinfo header", &incapsulaDetector{}, "", http.Header{"X-Iinfo": {"9-12345-0 0NNN RT(1) q(0) r(0)"}}},
{"Incapsula cookies", &incapsulaDetector{}, "", http.Header{"Set-Cookie": {"visid_incap_123=abc; path=/", "incap_ses_456=def; path=/"}}},
{"Incapsula incident page", &incapsulaDetector{}, `Request unsuccessful. Incapsula incident ID: 123-456`, http.Header{}},
{"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{}},
{"ModSecurity block page", &modSecurityDetector{}, `<p>This error was generated by Mod_Security.</p>`, http.Header{}},
{"ModSecurity server token", &modSecurityDetector{}, `This error was generated by Mod_Security.`, http.Header{"Server": {"Apache Mod_Security"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf, _ := tt.detector.Detect(tt.body, tt.headers)
if conf <= 0.5 {
t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf)
}
})
}
}
// TestWAFDetectors_Negative keeps the detectors quiet on prose that names the
// vendor and on unrelated proxy headers, so a mention or a coincidental header
// cannot trip a WAF finding.
func TestWAFDetectors_Negative(t *testing.T) {
tests := []struct {
name string
detector fw.Detector
body string
headers http.Header
}{
{"Cloudflare plain proxy", &cloudflareWAFDetector{}, "", http.Header{"Cf-Ray": {"8a1-EWR"}}},
{"Cloudflare prose", &cloudflareWAFDetector{}, `<p>We compared Cloudflare and Fastly for WAF coverage.</p>`, http.Header{}},
{"Sucuri prose", &sucuriDetector{}, `<p>Sucuri is a popular website firewall.</p>`, http.Header{}},
{"Incapsula prose", &incapsulaDetector{}, `<p>Imperva Incapsula competes with Cloudflare.</p>`, http.Header{}},
{"F5 unrelated body", &bigipASMDetector{}, `<p>Our support team can help with your F5 BIG-IP setup.</p>`, http.Header{}},
{"ModSecurity prose", &modSecurityDetector{}, `<p>We enabled ModSecurity rules on the origin.</p>`, http.Header{}},
{"plain nginx", &sucuriDetector{}, "", http.Header{"Server": {"nginx/1.25.3"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf, _ := tt.detector.Detect(tt.body, tt.headers)
if conf > 0.5 {
t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf)
}
})
}
}