Files
sif/internal/scan/cors_test.go
T
vmfunc 9401aa669e feat(scan): add cors, open-redirect and reflected-xss probes
three active web-vuln probes wired into the per-target loop:

- cors: crafts attacker origins (evil sentinel, null, prefix/suffix
  bypass, http downgrade) and flags responses that reflect them in
  access-control-allow-origin, ranking reflection+credentials high.
- redirect: injects a controlled sentinel host plus bypass variants
  (//, https:/, backslash, null-byte, userinfo @) into redirect-prone
  params and catches 30x location, meta-refresh and js redirects that
  resolve off-site.
- xss: injects a unique canary wrapped in breaking chars, classifies
  the reflection context (html/attribute/script) and reports only the
  chars that survive unescaped where they matter, so escaped
  reflections don't false-positive.

all route through httpx.Client so proxy/-H/-cookie/-rate-limit apply.
hermetic httptest coverage plus integration testbed entries.
2026-06-09 18:11:38 -07:00

141 lines
4.5 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
// reflectingCORS echoes the Origin into Access-Control-Allow-Origin and sets
// credentials, the exploitable misconfiguration.
func reflectingCORS() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
w.WriteHeader(http.StatusOK)
}))
}
func TestCORS_ReflectsArbitraryOrigin(t *testing.T) {
srv := reflectingCORS()
defer srv.Close()
result, err := CORS(srv.URL, 5*time.Second, 3, "")
if err != nil {
t.Fatalf("CORS: %v", err)
}
if result == nil || len(result.Findings) == 0 {
t.Fatalf("expected cors findings on reflecting server, got %+v", result)
}
// the reflecting server echoes every crafted origin with credentials,
// so each finding should be high severity.
var sawEvil bool
for _, f := range result.Findings {
if f.OriginTested == corsEvilOrigin {
sawEvil = true
if !f.AllowCredentials {
t.Errorf("expected credentials flagged for evil origin, got %+v", f)
}
if f.Severity != "high" {
t.Errorf("expected high severity for reflection+creds, got %s", f.Severity)
}
}
}
if !sawEvil {
t.Errorf("expected the sentinel evil origin to be reflected, got %+v", result.Findings)
}
}
func TestCORS_SeverityWithoutCredentials(t *testing.T) {
// reflects the origin but never grants credentials - medium, not high.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
result, err := CORS(srv.URL, 5*time.Second, 3, "")
if err != nil {
t.Fatalf("CORS: %v", err)
}
if result == nil || len(result.Findings) == 0 {
t.Fatalf("expected reflection findings, got %+v", result)
}
for _, f := range result.Findings {
if f.AllowCredentials {
t.Errorf("did not expect credentials, got %+v", f)
}
if f.Severity != "medium" {
t.Errorf("expected medium severity without creds, got %s", f.Severity)
}
}
}
func TestCORS_NoFalsePositiveOnSafeServer(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
}{
{
name: "ignores origin entirely",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
},
},
{
name: "returns its own fixed origin",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://trusted.example.com")
w.WriteHeader(http.StatusOK)
},
},
{
name: "plain wildcard, no credentials",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(tt.handler)
defer srv.Close()
result, err := CORS(srv.URL, 5*time.Second, 3, "")
if err != nil {
t.Fatalf("CORS: %v", err)
}
if result != nil && len(result.Findings) > 0 {
t.Errorf("expected no findings on safe server, got %+v", result.Findings)
}
})
}
}
func TestCORSResult_ResultType(t *testing.T) {
r := &CORSResult{}
if r.ResultType() != "cors" {
t.Errorf("expected result type 'cors', got %q", r.ResultType())
}
}