fix(scan): resolve favicon href against origin to stop pathful-target misfetch (#350)

This commit is contained in:
Tigah
2026-07-22 12:56:12 -07:00
committed by GitHub
parent a435c048a4
commit 75597fc90d
2 changed files with 35 additions and 14 deletions
+12 -14
View File
@@ -17,6 +17,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
@@ -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
}
@@ -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
ref, err := url.Parse(href)
if err != nil {
return href
}
if strings.HasPrefix(href, "/") {
return base + href
}
return base + "/" + href
return baseURL.ResolveReference(ref).String()
}
// ResultType identifies favicon findings for the result registry.
+23
View File
@@ -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" {