Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion internal/scan/securityheaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func SecurityHeaders(url string, timeout time.Duration, logdir string) (Security
// header-only scan: drain on close so the conn is returned to the pool.
defer httpx.DrainClose(resp)

results := gradeSecurityHeaders(resp.Header, strings.HasPrefix(url, "https://"))
results := gradeSecurityHeaders(resp.Header, responseIsHTTPS(resp, url))

for _, r := range results {
line := r.Header + " " + r.Note
Expand All @@ -96,6 +96,18 @@ func SecurityHeaders(url string, timeout time.Duration, logdir string) (Security
return results, nil
}

// responseIsHTTPS reports whether the response was actually served over
// https. the client follows redirects, so a request that started as
// http:// can end up served over https:// (or vice versa); the final
// scheme lives on resp.Request.URL, not the originally requested url.
// falls back to the requested url's scheme if that's unavailable.
func responseIsHTTPS(resp *http.Response, requestedURL string) bool {
if resp != nil && resp.Request != nil && resp.Request.URL != nil {
return resp.Request.URL.Scheme == "https"
}
return strings.HasPrefix(requestedURL, "https://")
}

func gradeSecurityHeaders(header http.Header, https bool) SecurityHeaderResults {
var results SecurityHeaderResults

Expand Down
54 changes: 54 additions & 0 deletions internal/scan/securityheaders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package scan
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
)
Expand Down Expand Up @@ -151,6 +152,59 @@ func TestGradeSecurityHeaders_Disclosure(t *testing.T) {
}
}

func TestResponseIsHTTPS_UsesFinalRequestScheme(t *testing.T) {
httpURL, err := url.Parse("http://example.com")
if err != nil {
t.Fatal(err)
}
httpsURL, err := url.Parse("https://example.com")
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
resp *http.Response
requestedURL string
want bool
}{
{
// the requested url was http, but the client followed a redirect
// to https; hsts must be graded against the actual scheme.
name: "redirected from http to https",
resp: &http.Response{Request: &http.Request{URL: httpsURL}},
requestedURL: "http://example.com",
want: true,
},
{
name: "redirected from https to http",
resp: &http.Response{Request: &http.Request{URL: httpURL}},
requestedURL: "https://example.com",
want: false,
},
{
name: "no redirect, stays http",
resp: &http.Response{Request: &http.Request{URL: httpURL}},
requestedURL: "http://example.com",
want: false,
},
{
name: "no request on response falls back to requested url",
resp: &http.Response{},
requestedURL: "https://example.com",
want: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := responseIsHTTPS(tt.resp, tt.requestedURL); got != tt.want {
t.Errorf("responseIsHTTPS() = %v, want %v", got, tt.want)
}
})
}
}

func TestSecurityHeaders_LiveResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
Expand Down
Loading