security: validate URLs to prevent SSRF in content fetching endpoints (#8476)

User-supplied URLs passed to GetContentURIAsBase64() and downloadFile()
were fetched without validation, allowing SSRF attacks against internal
services. Added URL validation that blocks private IPs, loopback,
link-local, and cloud metadata endpoints before fetching.

Co-authored-by: kolega.dev <faizan@kolega.ai>
This commit is contained in:
Kolega.dev
2026-02-10 14:14:14 +00:00
committed by GitHub
parent 08eeed61f4
commit 780877d1d0
4 changed files with 186 additions and 0 deletions

View File

@@ -23,10 +23,15 @@ import (
"github.com/mudler/LocalAI/core/backend"
model "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/xlog"
)
func downloadFile(url string) (string, error) {
if err := utils.ValidateExternalURL(url); err != nil {
return "", fmt.Errorf("URL validation failed: %w", err)
}
// Get the data
resp, err := http.Get(url)
if err != nil {

View File

@@ -21,6 +21,10 @@ var dataURIPattern = regexp.MustCompile(`^data:([^;]+);base64,`)
// GetContentURIAsBase64 checks if the string is an URL, if it's an URL downloads the content in memory encodes it in base64 and returns the base64 string, otherwise returns the string by stripping base64 data headers
func GetContentURIAsBase64(s string) (string, error) {
if strings.HasPrefix(s, "http") || strings.HasPrefix(s, "https") {
if err := ValidateExternalURL(s); err != nil {
return "", fmt.Errorf("URL validation failed: %w", err)
}
// download the image
resp, err := base64DownloadClient.Get(s)
if err != nil {

78
pkg/utils/urlfetch.go Normal file
View File

@@ -0,0 +1,78 @@
package utils
import (
"fmt"
"net"
"net/url"
"strings"
)
// ValidateExternalURL checks that the given URL does not point to a private,
// loopback, link-local, or otherwise internal network address. This prevents
// Server-Side Request Forgery (SSRF) attacks where a user-supplied URL could
// be used to probe internal services or cloud metadata endpoints.
func ValidateExternalURL(rawURL string) error {
parsed, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
scheme := strings.ToLower(parsed.Scheme)
if scheme != "http" && scheme != "https" {
return fmt.Errorf("unsupported URL scheme: %s", scheme)
}
hostname := parsed.Hostname()
if hostname == "" {
return fmt.Errorf("URL has no hostname")
}
// Block well-known internal hostnames
lower := strings.ToLower(hostname)
if lower == "localhost" || strings.HasSuffix(lower, ".local") {
return fmt.Errorf("requests to internal hosts are not allowed")
}
// Block cloud metadata service hostnames
if lower == "metadata.google.internal" || lower == "instance-data" {
return fmt.Errorf("requests to cloud metadata services are not allowed")
}
ips, err := net.LookupHost(hostname)
if err != nil {
return fmt.Errorf("failed to resolve hostname: %w", err)
}
for _, ipStr := range ips {
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("unable to parse resolved IP: %s", ipStr)
}
if !isPublicIP(ip) {
return fmt.Errorf("requests to internal network addresses are not allowed")
}
}
return nil
}
func isPublicIP(ip net.IP) bool {
if ip.IsLoopback() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsPrivate() ||
ip.IsUnspecified() {
return false
}
// Block IPv4-mapped IPv6 addresses that wrap private IPv4
if ip4 := ip.To4(); ip4 != nil {
return !ip4.IsLoopback() &&
!ip4.IsLinkLocalUnicast() &&
!ip4.IsPrivate() &&
!ip4.IsUnspecified()
}
return true
}

View File

@@ -0,0 +1,99 @@
package utils_test
import (
. "github.com/mudler/LocalAI/pkg/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("utils/urlfetch tests", func() {
Context("ValidateExternalURL", func() {
It("allows valid external HTTPS URLs", func() {
err := ValidateExternalURL("https://example.com/image.png")
Expect(err).To(BeNil())
})
It("allows valid external HTTP URLs", func() {
err := ValidateExternalURL("http://example.com/image.png")
Expect(err).To(BeNil())
})
It("blocks localhost", func() {
err := ValidateExternalURL("http://localhost/secret")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks 127.0.0.1", func() {
err := ValidateExternalURL("http://127.0.0.1/secret")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks private 10.x.x.x range", func() {
err := ValidateExternalURL("http://10.0.0.1/secret")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks private 172.16.x.x range", func() {
err := ValidateExternalURL("http://172.16.0.1/secret")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks private 192.168.x.x range", func() {
err := ValidateExternalURL("http://192.168.1.1/secret")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks link-local 169.254.x.x (AWS metadata)", func() {
err := ValidateExternalURL("http://169.254.169.254/latest/meta-data/")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks unsupported schemes", func() {
err := ValidateExternalURL("ftp://example.com/file")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("unsupported URL scheme"))
})
It("blocks file:// scheme", func() {
err := ValidateExternalURL("file:///etc/passwd")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("unsupported URL scheme"))
})
It("blocks URLs with no hostname", func() {
err := ValidateExternalURL("http:///path")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("no hostname"))
})
It("blocks .local hostnames", func() {
err := ValidateExternalURL("http://myservice.local/api")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks metadata.google.internal", func() {
err := ValidateExternalURL("http://metadata.google.internal/computeMetadata/v1/")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("metadata"))
})
It("blocks 0.0.0.0", func() {
err := ValidateExternalURL("http://0.0.0.0/")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
It("blocks IPv6 loopback ::1", func() {
err := ValidateExternalURL("http://[::1]/secret")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("internal"))
})
})
})