From 04c779994bb0168350fd246cb6a6b901b85db7af Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sat, 9 May 2026 15:05:06 +1200 Subject: [PATCH] Security: Block internal IP access by default in HTML check (GHSA-j3fj-qppj-fmmc) This addresses an incomplete fix for GHSA-6jxm-fv7w-rw5j which did not restrict access to internal IP addresses. --- cmd/root.go | 2 +- internal/htmlcheck/css.go | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 1224abf..5886d88 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -107,7 +107,7 @@ func init() { rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert") rootCmd.Flags().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set CORS origin(s) for the API, comma-separated (eg: example.com,foo.com)") rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts") - rootCmd.Flags().BoolVar(&config.AllowInternalHTTPRequests, "allow-internal-http-requests", config.AllowInternalHTTPRequests, "Allow link-checker & screenshots to access internal IP addresses") + rootCmd.Flags().BoolVar(&config.AllowInternalHTTPRequests, "allow-internal-http-requests", config.AllowInternalHTTPRequests, "Allow link checker, HTML checker & screenshots to access internal IP addresses") rootCmd.Flags().StringVar(&config.EnableSpamAssassin, "enable-spamassassin", config.EnableSpamAssassin, "Enable integration with SpamAssassin") rootCmd.Flags().BoolVar(&config.AllowUntrustedTLS, "allow-untrusted-tls", config.AllowUntrustedTLS, "Do not verify HTTPS certificates (link checker & screenshots)") rootCmd.Flags().BoolVar(&config.DisableHTTPCompression, "disable-http-compression", config.DisableHTTPCompression, "Disable HTTP compression support (web UI & API)") diff --git a/internal/htmlcheck/css.go b/internal/htmlcheck/css.go index e52c213..6dc471f 100644 --- a/internal/htmlcheck/css.go +++ b/internal/htmlcheck/css.go @@ -190,7 +190,7 @@ func inlineRemoteCSS(h string) (string, error) { // It requires the HTTP response code to be 200 and the content-type to be text/css. // It will download a maximum of 5MB. func downloadCSSToBytes(url string) ([]byte, error) { - client := newSafeHTTPClient() + client := safeHTTPClient() req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, nil) if err != nil { return nil, err @@ -272,17 +272,15 @@ func testInlineStyles(doc *goquery.Document) map[string]int { return matches } -func newSafeHTTPClient() *http.Client { +func safeHTTPClient() *http.Client { dialer := &net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, } tr := &http.Transport{ - Proxy: nil, // avoid env proxy surprises unless you explicitly want it - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - return dialer.DialContext(ctx, network, address) - }, + Proxy: nil, // avoid env proxy surprises unless you explicitly want it + DialContext: safeDialContext(dialer), TLSHandshakeTimeout: 5 * time.Second, ResponseHeaderTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, @@ -308,3 +306,29 @@ func newSafeHTTPClient() *http.Client { return client } + +// safeDialContext is the same pattern as linkcheck/status.go::safeDialContext +// — copy the function (or factor a shared helper into internal/tools/net.go). +func safeDialContext(dialer *net.Dialer) func(ctx context.Context, network, address string) (net.Conn, error) { + return func(ctx context.Context, network, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, err + } + + if !config.AllowInternalHTTPRequests { + for _, ip := range ips { + if tools.IsInternalIP(ip.IP) { + return nil, fmt.Errorf("blocked request to %s (%s): private/reserved address", host, ip) + } + } + } + + return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), port)) + } +}