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
26 changes: 12 additions & 14 deletions internal/scan/favicon.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -117,7 +118,9 @@ func Favicon(targetURL string, timeout time.Duration, logdir string) (*FaviconRe
// homepage html. it returns the url it pulled the bytes from so the report shows
// exactly which icon was hashed.
func fetchFavicon(client *http.Client, base string) (string, []byte, error) {
iconURL := base + "/favicon.ico"
// the well-known icon always lives at the origin root, so resolve it there
// rather than appending to a target that may carry a path.
iconURL := resolveFaviconURL(base, "/favicon.ico")
if data, err := getFaviconBytes(client, iconURL); err == nil {
return iconURL, data, nil
}
Expand Down Expand Up @@ -191,23 +194,18 @@ func declaredFaviconHref(client *http.Client, base string) (string, error) {
}

// resolveFaviconURL turns a possibly-relative href into an absolute url against
// the target base. an absolute href is returned as-is.
// the target base, browser-style (root-relative anchors at the origin, not the
// target's path). unparsable input falls back to the raw href.
func resolveFaviconURL(base, href string) string {
if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
baseURL, err := url.Parse(base)
if err != nil {
return href
}
if strings.HasPrefix(href, "//") {
// scheme-relative; inherit the base scheme.
scheme := "https:"
if strings.HasPrefix(base, "http://") {
scheme = "http:"
}
return scheme + href
}
if strings.HasPrefix(href, "/") {
return base + href
ref, err := url.Parse(href)
if err != nil {
return href
}
return base + "/" + href
return baseURL.ResolveReference(ref).String()
}

// ResultType identifies favicon findings for the result registry.
Expand Down
23 changes: 23 additions & 0 deletions internal/scan/favicon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@ func TestFavicon_NoIcon(t *testing.T) {
}
}

func TestResolveFaviconURL(t *testing.T) {
cases := []struct {
name string
base string
href string
want string
}{
// see resolveFaviconURL's doc comment (favicon.go) for the anchoring rule.
{"root-relative against pathful base", "https://example.com/app", "/favicon.ico", "https://example.com/favicon.ico"},
{"root-relative against bare base", "https://example.com", "/static/icon.png", "https://example.com/static/icon.png"},
{"absolute href kept", "https://example.com", "https://cdn.example.net/f.ico", "https://cdn.example.net/f.ico"},
{"scheme-relative inherits https", "https://example.com", "//cdn.example.net/f.ico", "https://cdn.example.net/f.ico"},
{"scheme-relative inherits http", "http://example.com", "//cdn.example.net/f.ico", "http://cdn.example.net/f.ico"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := resolveFaviconURL(tc.base, tc.href); got != tc.want {
t.Errorf("resolveFaviconURL(%q, %q) = %q, want %q", tc.base, tc.href, got, tc.want)
}
})
}
}

func TestFaviconResult_ResultType(t *testing.T) {
r := &FaviconResult{}
if r.ResultType() != "favicon" {
Expand Down
Loading