Skip to content

Commit da12973

Browse files
committed
fix(scan): resolve favicon href against origin to stop pathful-target misfetch
1 parent 7ea1cd2 commit da12973

2 files changed

Lines changed: 35 additions & 14 deletions

File tree

internal/scan/favicon.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"io"
1919
"net/http"
20+
"net/url"
2021
"regexp"
2122
"strings"
2223
"time"
@@ -117,7 +118,9 @@ func Favicon(targetURL string, timeout time.Duration, logdir string) (*FaviconRe
117118
// homepage html. it returns the url it pulled the bytes from so the report shows
118119
// exactly which icon was hashed.
119120
func fetchFavicon(client *http.Client, base string) (string, []byte, error) {
120-
iconURL := base + "/favicon.ico"
121+
// the well-known icon always lives at the origin root, so resolve it there
122+
// rather than appending to a target that may carry a path.
123+
iconURL := resolveFaviconURL(base, "/favicon.ico")
121124
if data, err := getFaviconBytes(client, iconURL); err == nil {
122125
return iconURL, data, nil
123126
}
@@ -191,23 +194,18 @@ func declaredFaviconHref(client *http.Client, base string) (string, error) {
191194
}
192195

193196
// resolveFaviconURL turns a possibly-relative href into an absolute url against
194-
// the target base. an absolute href is returned as-is.
197+
// the target base, browser-style (root-relative anchors at the origin, not the
198+
// target's path). unparsable input falls back to the raw href.
195199
func resolveFaviconURL(base, href string) string {
196-
if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
200+
baseURL, err := url.Parse(base)
201+
if err != nil {
197202
return href
198203
}
199-
if strings.HasPrefix(href, "//") {
200-
// scheme-relative; inherit the base scheme.
201-
scheme := "https:"
202-
if strings.HasPrefix(base, "http://") {
203-
scheme = "http:"
204-
}
205-
return scheme + href
206-
}
207-
if strings.HasPrefix(href, "/") {
208-
return base + href
204+
ref, err := url.Parse(href)
205+
if err != nil {
206+
return href
209207
}
210-
return base + "/" + href
208+
return baseURL.ResolveReference(ref).String()
211209
}
212210

213211
// ResultType identifies favicon findings for the result registry.

internal/scan/favicon_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,29 @@ func TestFavicon_NoIcon(t *testing.T) {
110110
}
111111
}
112112

113+
func TestResolveFaviconURL(t *testing.T) {
114+
cases := []struct {
115+
name string
116+
base string
117+
href string
118+
want string
119+
}{
120+
// see resolveFaviconURL's doc comment (favicon.go) for the anchoring rule.
121+
{"root-relative against pathful base", "https://example.com/app", "/favicon.ico", "https://example.com/favicon.ico"},
122+
{"root-relative against bare base", "https://example.com", "/static/icon.png", "https://example.com/static/icon.png"},
123+
{"absolute href kept", "https://example.com", "https://cdn.example.net/f.ico", "https://cdn.example.net/f.ico"},
124+
{"scheme-relative inherits https", "https://example.com", "//cdn.example.net/f.ico", "https://cdn.example.net/f.ico"},
125+
{"scheme-relative inherits http", "http://example.com", "//cdn.example.net/f.ico", "http://cdn.example.net/f.ico"},
126+
}
127+
for _, tc := range cases {
128+
t.Run(tc.name, func(t *testing.T) {
129+
if got := resolveFaviconURL(tc.base, tc.href); got != tc.want {
130+
t.Errorf("resolveFaviconURL(%q, %q) = %q, want %q", tc.base, tc.href, got, tc.want)
131+
}
132+
})
133+
}
134+
}
135+
113136
func TestFaviconResult_ResultType(t *testing.T) {
114137
r := &FaviconResult{}
115138
if r.ResultType() != "favicon" {

0 commit comments

Comments
 (0)