From da12973605792a2cbbf19de519f2ef6f24c18e64 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:56:10 -0700 Subject: [PATCH] fix(scan): resolve favicon href against origin to stop pathful-target misfetch --- internal/scan/favicon.go | 26 ++++++++++++-------------- internal/scan/favicon_test.go | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/internal/scan/favicon.go b/internal/scan/favicon.go index 71af16fd..ade91047 100644 --- a/internal/scan/favicon.go +++ b/internal/scan/favicon.go @@ -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 - } - 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. diff --git a/internal/scan/favicon_test.go b/internal/scan/favicon_test.go index a06f78fc..b0de1df9 100644 --- a/internal/scan/favicon_test.go +++ b/internal/scan/favicon_test.go @@ -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" {