diff --git a/cmd/scan/curl.go b/cmd/scan/curl.go index 3d9159e..c243a4d 100644 --- a/cmd/scan/curl.go +++ b/cmd/scan/curl.go @@ -47,17 +47,19 @@ func NewCURLScanCmd() (scanCmd *cobra.Command) { log.Fatal(err) } - rpr, _, err := scan.WithAllVulnsScans().Execute() + rpr, _, err := scan.WithAllVulnsScans().WithAllBestPracticesScans().Execute() if err != nil { log.Fatal(err) } - if !rpr.HasVulnerability() { - log.Println("Congratulations! No vulnerability has been discovered!") + for _, r := range rpr.GetVulnerabilityReports() { + log.Println(r) } - for _, r := range rpr.GetVulnerabilityReports() { - log.Fatalln(r) + if !rpr.HasVulnerability() { + log.Println("Congratulations! No vulnerability has been discovered!") + } else { + log.Fatalln("There is one or more vulnerabilies you should know.") } }, } diff --git a/cmd/scan/openapi.go b/cmd/scan/openapi.go index 8451fca..7c3e1ea 100644 --- a/cmd/scan/openapi.go +++ b/cmd/scan/openapi.go @@ -43,17 +43,19 @@ func NewOpenAPIScanCmd() (scanCmd *cobra.Command) { log.Fatal(err) } - rpr, _, err := scan.WithAllVulnsScans().Execute() + rpr, _, err := scan.WithAllVulnsScans().WithAllBestPracticesScans().Execute() if err != nil { log.Fatal(err) } - if !rpr.HasVulnerability() { - log.Println("Congratulations! No vulnerability has been discovered!") + for _, r := range rpr.GetVulnerabilityReports() { + log.Println(r) } - for _, r := range rpr.GetVulnerabilityReports() { - log.Fatalln(r) + if !rpr.HasVulnerability() { + log.Println("Congratulations! No vulnerability has been discovered!") + } else { + log.Fatalln("There is one or more vulnerabilies you should know.") } }, } diff --git a/scan/best_practices.go b/scan/best_practices.go new file mode 100644 index 0000000..66fd427 --- /dev/null +++ b/scan/best_practices.go @@ -0,0 +1,13 @@ +package scan + +import ( + bestpractices "github.com/cerberauth/vulnapi/scan/best_practices" +) + +func (s *Scan) WithHTTPHeadersBestPracticesScan() *Scan { + return s.AddScanHandler(bestpractices.HTTPHeadersBestPracticesScanHandler) +} + +func (s *Scan) WithAllBestPracticesScans() *Scan { + return s.WithHTTPHeadersBestPracticesScan() +} diff --git a/scan/best_practices/http_headers.go b/scan/best_practices/http_headers.go new file mode 100644 index 0000000..658cc97 --- /dev/null +++ b/scan/best_practices/http_headers.go @@ -0,0 +1,151 @@ +package bestpractices + +import ( + "net/http" + "strings" + + "github.com/cerberauth/vulnapi/internal/auth" + restapi "github.com/cerberauth/vulnapi/internal/rest_api" + "github.com/cerberauth/vulnapi/report" +) + +const ( + CSPHTTPHeader = "Content-Security-Policy" + HSTSHTTPHeader = "Strict-Transport-Security" + CORSOriginHTTPHeader = "Access-Control-Allow-Origin" + XContentTypeOptionsHTTPHeader = "X-Content-Type-Options" + XFrameOptionsHTTPHeader = "X-Frame-Options" +) + +const ( + CSPHTTPHeaderSeverityLevel = 1 + CSPHTTPHeaderIsNotSetVulnerabilityName = "CSP Header is not set" + CSPHTTPHeaderIsNotSetVulnerabilityDescription = "No Content Security Policy (CSP) Header has been detected in HTTP Response." + CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityName = "CSP frame-ancestors policy is not set" + CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityDescription = "No frame-ancestors policy has been set in CSP HTTP Response Header." + + HSTSHTTPHeaderSeverityLevel = 1 + HSTSHTTPHeaderIsNotSetVulnerabilityName = "HSTS Header is not set" + HSTSHTTPHeaderIsNotSetVulnerabilityDescription = "No HSTS Header has been detected in HTTP Response." + + CORSHTTPHeaderSeverityLevel = 1 + CORSHTTPHeaderIsNotSetVulnerabilityName = "CORS Header is not set" + CORSHTTPHeaderIsNotSetVulnerabilityDescription = "No CORS Header has been detected in HTTP Response." + CORSHTTPHeaderIsPermisiveVulnerabilityName = "CORS Header is set but permissive" + CORSHTTPHeaderIsPermisiveVulnerabilityDescription = "CORS Header has been detected in HTTP Response but is permissive." + + XContentTypeOptionsHTTPHeaderIsNotSetSeverityLevel = 1 + XContentTypeOptionsHTTPHeaderIsNotSetVulnerabilityName = "X-Content-Type-Options Header is not set" + XContentTypeOptionsHTTPHeaderIsNotSetVulnerabilityDescription = "No X-Content-Type-Options Header has been detected in HTTP Response." + + XFrameOptionsHTTPHeaderIsNotSetSeverityLevel = 1 + XFrameOptionsHTTPHeaderIsNotSetVulnerabilityName = "X-Frame-Options Header is not set" + XFrameOptionsHTTPHeaderIsNotSetVulnerabilityDescription = "No X-Frame-Options Header has been detected in HTTP Response." +) + +func checkCSPHeader(o *auth.Operation, headers http.Header, r *report.ScanReport) bool { + cspHeader := headers.Get(CSPHTTPHeader) + if cspHeader == "" { + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: CSPHTTPHeaderSeverityLevel, + Name: CSPHTTPHeaderIsNotSetVulnerabilityName, + Description: CSPHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + }) + + return false + } + + directives := strings.Split(cspHeader, ";") + for _, directive := range directives { + directive = strings.TrimSpace(directive) + if strings.HasPrefix(directive, "frame-ancestors") { + // Check if frame-ancestors directive is not equal to "none" + if strings.Contains(directive, "none") { + return true + } + } + } + + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: CSPHTTPHeaderSeverityLevel, + Name: CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityName, + Description: CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityDescription, + Url: o.Url, + }) + + return false +} + +func CheckCORSAllowOrigin(o *auth.Operation, headers http.Header, r *report.ScanReport) bool { + allowOrigin := headers.Get(CORSOriginHTTPHeader) + if allowOrigin == "" { + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: CORSHTTPHeaderSeverityLevel, + Name: CORSHTTPHeaderIsNotSetVulnerabilityName, + Description: CORSHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + }) + + return false + } + + // Check if the Access-Control-Allow-Origin header is not "*" (wildcard) + if allowOrigin != "*" { + return true + } + + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: CORSHTTPHeaderSeverityLevel, + Name: CORSHTTPHeaderIsPermisiveVulnerabilityName, + Description: CORSHTTPHeaderIsPermisiveVulnerabilityDescription, + Url: o.Url, + }) + + return false +} + +func HTTPHeadersBestPracticesScanHandler(o *auth.Operation, ss auth.SecurityScheme) (*report.ScanReport, error) { + r := report.NewScanReport() + token := ss.GetValidValue().(string) + + ss.SetAttackValue(token) + vsa := restapi.ScanRestAPI(o, ss) + r.AddScanAttempt(vsa).End() + + if vsa.Err != nil { + return r, vsa.Err + } + + checkCSPHeader(o, vsa.Response.Header, r) + CheckCORSAllowOrigin(o, vsa.Response.Header, r) + + if hstsHeader := vsa.Response.Header.Get(HSTSHTTPHeader); hstsHeader == "" { + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: HSTSHTTPHeaderSeverityLevel, + Name: HSTSHTTPHeaderIsNotSetVulnerabilityName, + Description: HSTSHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + }) + } + + if xContentTypeOptionsHeader := vsa.Response.Header.Get(XContentTypeOptionsHTTPHeader); xContentTypeOptionsHeader == "" { + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: XContentTypeOptionsHTTPHeaderIsNotSetSeverityLevel, + Name: XContentTypeOptionsHTTPHeaderIsNotSetVulnerabilityName, + Description: XContentTypeOptionsHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + }) + } + + if xFrameOptionsHeader := vsa.Response.Header.Get(XFrameOptionsHTTPHeader); xFrameOptionsHeader == "" { + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: XFrameOptionsHTTPHeaderIsNotSetSeverityLevel, + Name: XFrameOptionsHTTPHeaderIsNotSetVulnerabilityName, + Description: XFrameOptionsHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + }) + } + + return r, nil +} diff --git a/scan/best_practices/http_headers_test.go b/scan/best_practices/http_headers_test.go new file mode 100644 index 0000000..3025d91 --- /dev/null +++ b/scan/best_practices/http_headers_test.go @@ -0,0 +1,277 @@ +package bestpractices_test + +import ( + "net/http" + "testing" + + "github.com/cerberauth/vulnapi/internal/auth" + "github.com/cerberauth/vulnapi/report" + bestpractices "github.com/cerberauth/vulnapi/scan/best_practices" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func getValidHTTPHeaders(o *auth.Operation) *http.Header { + header := http.Header{} + header.Add(bestpractices.CSPHTTPHeader, "frame-ancestors 'none'") + header.Add(bestpractices.CORSOriginHTTPHeader, "http://localhost:8080") + header.Add(bestpractices.HSTSHTTPHeader, "max-age=63072000; includeSubDomains; preload") + header.Add(bestpractices.XContentTypeOptionsHTTPHeader, "nosniff") + header.Add(bestpractices.XFrameOptionsHTTPHeader, "DENY") + + return &header +} + +func TestHTTPHeadersBestPracticesScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + + header := getValidHTTPHeaders(&o) + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.False(t, report.HasVulnerabilityReport()) +} + +func TestHTTPHeadersBestPracticesWithoutCSPScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.CSPHTTPHeaderSeverityLevel, + Name: bestpractices.CSPHTTPHeaderIsNotSetVulnerabilityName, + Description: bestpractices.CSPHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Del(bestpractices.CSPHTTPHeader) + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithoutFrameAncestorsCSPDirectiveScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.CSPHTTPHeaderSeverityLevel, + Name: bestpractices.CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityName, + Description: bestpractices.CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Set(bestpractices.CSPHTTPHeader, "default-src 'self' http://example.com; connect-src 'none'") + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithNotNoneFrameAncestorsCSPDirectiveScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.CSPHTTPHeaderSeverityLevel, + Name: bestpractices.CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityName, + Description: bestpractices.CSPHTTPHeaderFrameAncestorsIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Set(bestpractices.CSPHTTPHeader, "default-src 'self' http://example.com; connect-src 'none'; frame-ancestors 'http://example.com'") + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithoutCORSScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.CORSHTTPHeaderSeverityLevel, + Name: bestpractices.CORSHTTPHeaderIsNotSetVulnerabilityName, + Description: bestpractices.CORSHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Del(bestpractices.CORSOriginHTTPHeader) + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithPermissiveCORSScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.CORSHTTPHeaderSeverityLevel, + Name: bestpractices.CORSHTTPHeaderIsPermisiveVulnerabilityName, + Description: bestpractices.CORSHTTPHeaderIsPermisiveVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Set(bestpractices.CORSOriginHTTPHeader, "*") + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithoutHSTSScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.HSTSHTTPHeaderSeverityLevel, + Name: bestpractices.HSTSHTTPHeaderIsNotSetVulnerabilityName, + Description: bestpractices.HSTSHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Del(bestpractices.HSTSHTTPHeader) + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithoutXContentTypeOptionsScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.XContentTypeOptionsHTTPHeaderIsNotSetSeverityLevel, + Name: bestpractices.XContentTypeOptionsHTTPHeaderIsNotSetVulnerabilityName, + Description: bestpractices.XContentTypeOptionsHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Del(bestpractices.XContentTypeOptionsHTTPHeader) + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} + +func TestHTTPHeadersBestPracticesWithoutXFrameOptionsScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + o := auth.Operation{ + Method: "GET", + Url: "http://localhost:8080/", + } + vulnerabilityReport := report.VulnerabilityReport{ + SeverityLevel: bestpractices.XFrameOptionsHTTPHeaderIsNotSetSeverityLevel, + Name: bestpractices.XFrameOptionsHTTPHeaderIsNotSetVulnerabilityName, + Description: bestpractices.XFrameOptionsHTTPHeaderIsNotSetVulnerabilityDescription, + Url: o.Url, + } + + header := getValidHTTPHeaders(&o) + header.Del(bestpractices.XFrameOptionsHTTPHeader) + httpmock.RegisterResponder(o.Method, o.Url, httpmock.NewBytesResponder(204, nil).HeaderAdd(*header)) + + report, err := bestpractices.HTTPHeadersBestPracticesScanHandler(&o, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0], &vulnerabilityReport) +} diff --git a/scan/scan.go b/scan/scan.go index a57975e..db0a9c1 100644 --- a/scan/scan.go +++ b/scan/scan.go @@ -70,11 +70,10 @@ func (s *Scan) ExecuteOperation(o *auth.Operation) ([]error, error) { rep, err := handler(o, o.SecuritySchemes[0]) if err != nil { - println(err.Error()) errors = append(errors, err) - } else if rep != nil { - s.Reporter.AddReport(rep) } + + s.Reporter.AddReport(rep) } return errors, nil