Skip to content

Commit c54254c

Browse files
committed
Security: Prevent Server-Side Request Forgery (SSRF) via HTML Check API
1 parent c035139 commit c54254c

File tree

1 file changed

+22
-9
lines changed

1 file changed

+22
-9
lines changed

internal/htmlcheck/css.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func inlineRemoteCSS(h string) (string, error) {
141141
attributes := link.Attr
142142
for _, a := range attributes {
143143
if a.Key == "href" {
144-
if !isURL(a.Val) {
144+
if !isValidURL(a.Val) {
145145
// skip invalid URL
146146
continue
147147
}
@@ -151,9 +151,9 @@ func inlineRemoteCSS(h string) (string, error) {
151151
return h, nil
152152
}
153153

154-
resp, err := downloadToBytes(a.Val)
154+
resp, err := downloadCSSToBytes(a.Val)
155155
if err != nil {
156-
logger.Log().Warnf("[html-check] failed to download %s", a.Val)
156+
logger.Log().Warnf("[html-check] %s", err.Error())
157157
continue
158158
}
159159

@@ -182,8 +182,10 @@ func inlineRemoteCSS(h string) (string, error) {
182182
return newDoc, nil
183183
}
184184

185-
// DownloadToBytes returns a []byte slice from a URL
186-
func downloadToBytes(url string) ([]byte, error) {
185+
// DownloadCSSToBytes returns a []byte slice from a URL.
186+
// It requires the HTTP response code to be 200 and the content-type to be text/css.
187+
// It will download a maximum of 5MB.
188+
func downloadCSSToBytes(url string) ([]byte, error) {
187189
client := http.Client{
188190
Timeout: 5 * time.Second,
189191
}
@@ -200,18 +202,29 @@ func downloadToBytes(url string) ([]byte, error) {
200202
return nil, err
201203
}
202204

203-
body, err := io.ReadAll(resp.Body)
205+
ct := strings.ToLower(resp.Header.Get("content-type"))
206+
if !strings.Contains(ct, "text/css") {
207+
err := fmt.Errorf("invalid CSS content-type from %s: \"%s\" (expected \"text/css\")", url, ct)
208+
return nil, err
209+
}
210+
211+
// set a limit on the number of bytes to read - max 5MB
212+
limit := int64(5242880)
213+
limitedReader := &io.LimitedReader{R: resp.Body, N: limit}
214+
215+
body, err := io.ReadAll(limitedReader)
204216
if err != nil {
205217
return nil, err
206218
}
207219

208220
return body, nil
209221
}
210222

211-
// Test if str is a URL
212-
func isURL(str string) bool {
223+
// Test if the string is a supported URL.
224+
// The URL must have the "http" or "https" scheme, and must not contain any login info (http://user:pass@<host>).
225+
func isValidURL(str string) bool {
213226
u, err := url.Parse(str)
214-
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
227+
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != "" && u.User == nil
215228
}
216229

217230
// Test the HTML for inline CSS styles and styling attributes

0 commit comments

Comments
 (0)