Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion internal/component/faro/receiver/sourcemaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"io"
"net"
"io/fs"
"log/slog"
"net/http"
Expand Down Expand Up @@ -145,7 +146,15 @@ func newSourceMapsStore(log *slog.Logger, args SourceMapsArguments, metrics *sou
// client.

if cli == nil {
cli = &http.Client{Timeout: args.DownloadTimeout}
cli = &http.Client{
Timeout: args.DownloadTimeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if isUnsafeDownloadURL(req.URL.String()) {
return fmt.Errorf("refusing redirect to blocked address")
}
return nil
},
}
}
if fs == nil {
fs = osFileService{}
Expand Down Expand Up @@ -460,6 +469,10 @@ func (store *sourceMapsStoreImpl) downloadSourceMapContent(sourceURL string) (co
store.log.Debug("resolved absolute source map URL", "url", sourceURL, "sourceMapURL", sourceMapURL)
}

if !urlMatchesOrigins(resolvedSourceMapURL, store.args.DownloadFromOrigins) {
store.log.Debug("resolved source map url origin not allowed", "url", resolvedSourceMapURL)
return nil, "", fmt.Errorf("source map url origin not allowed")
}
store.log.Debug("attempting to download source map file", "url", resolvedSourceMapURL)
result, err = store.downloadFileContents(resolvedSourceMapURL)
if err != nil {
Expand All @@ -471,6 +484,10 @@ func (store *sourceMapsStoreImpl) downloadSourceMapContent(sourceURL string) (co
}

func (store *sourceMapsStoreImpl) downloadFileContents(url string) ([]byte, error) {
if isUnsafeDownloadURL(url) {
store.metrics.downloads.WithLabelValues(getOrigin(url), "blocked").Inc()
return nil, fmt.Errorf("refusing download to blocked address")
}
resp, err := store.cli.Get(url)
if err != nil {
store.metrics.downloads.WithLabelValues(getOrigin(url), "?").Inc()
Expand All @@ -492,6 +509,49 @@ func (store *sourceMapsStoreImpl) downloadFileContents(url string) ([]byte, erro

var reSourceMap = regexp.MustCompile("//[#@]\\s(source(?:Mapping)?URL)=\\s*(?P<url>\\S+)\r?\n?$")


func isBlockedIP(ip net.IP) bool {
if ip == nil {
return true
}
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() {
return true
}
// CGNAT / shared address space (RFC 6598)
if ip4 := ip.To4(); ip4 != nil && ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127 {
return true
}
return false
}

// isUnsafeDownloadURL rejects loopback/private/link-local/CGNAT targets for
// faro.receiver sourcemap downloads (client-controlled Filename / sourceMappingURL).
func isUnsafeDownloadURL(raw string) bool {
u, err := url.Parse(raw)
if err != nil || u.Hostname() == "" {
return true
}
if u.Scheme != "http" && u.Scheme != "https" {
return true
}
host := u.Hostname()
if ip := net.ParseIP(host); ip != nil {
return isBlockedIP(ip)
}
ips, err := net.LookupIP(host)
if err != nil || len(ips) == 0 {
// Unknown host: let the HTTP client fail. Still block when DNS
// succeeds and points at a non-global address.
return false
}
for _, ip := range ips {
if isBlockedIP(ip) {
return true
}
}
return false
}

func getOrigin(URL string) string {
// TODO(rfratto): why are we parsing this every time? Let's parse it once.

Expand Down
53 changes: 49 additions & 4 deletions internal/component/faro/receiver/sourcemaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path/filepath"
"net"
"testing"
"time"

Expand Down Expand Up @@ -100,7 +101,7 @@ func Test_sourceMapsStoreImpl_DownloadSuccess(t *testing.T) {
}

actual := transformException(logger, store, mockException(), "123")
require.Equal(t, []string{"http://localhost:1234/foo.js", "http://localhost:1234/foo.js.map"}, httpClient.requests)
require.Equal(t, []string{"http://example.com:1234/foo.js", "http://example.com:1234/foo.js.map"}, httpClient.requests)
require.Equal(t, expect, actual)
}

Expand Down Expand Up @@ -134,7 +135,7 @@ func Test_sourceMapsStoreImpl_DownloadError(t *testing.T) {

expect := mockException()
actual := transformException(logger, store, expect, "123")
require.Equal(t, []string{"http://localhost:1234/foo.js"}, httpClient.requests)
require.Equal(t, []string{"http://example.com:1234/foo.js"}, httpClient.requests)
require.Equal(t, expect, actual)
}

Expand Down Expand Up @@ -1076,13 +1077,13 @@ func mockException() *payload.Exception {
Frames: []payload.Frame{
{
Colno: 6,
Filename: "http://localhost:1234/foo.js",
Filename: "http://example.com:1234/foo.js",
Function: "eval",
Lineno: 5,
},
{
Colno: 5,
Filename: "http://localhost:1234/foo.js",
Filename: "http://example.com:1234/foo.js",
Function: "callUndefined",
Lineno: 6,
},
Expand All @@ -1109,3 +1110,47 @@ func newTestFileService() *testFileService {
reads: make([]string, 0),
}
}


func Test_isUnsafeDownloadURL(t *testing.T) {
t.Parallel()
require.True(t, isUnsafeDownloadURL("http://127.0.0.1/map.js"))
require.True(t, isUnsafeDownloadURL("http://169.254.169.254/latest/meta-data/"))
require.True(t, isUnsafeDownloadURL("http://10.0.0.1/x.js"))
require.True(t, isUnsafeDownloadURL("http://[::1]/8080/x.js"))
require.True(t, isUnsafeDownloadURL("file:///etc/passwd"))
}

func Test_isBlockedIP(t *testing.T) {
t.Parallel()
require.True(t, isBlockedIP(net.ParseIP("127.0.0.1")))
require.True(t, isBlockedIP(net.ParseIP("10.1.2.3")))
require.True(t, isBlockedIP(net.ParseIP("192.168.1.1")))
require.True(t, isBlockedIP(net.ParseIP("169.254.169.254")))
require.True(t, isBlockedIP(net.ParseIP("100.64.0.1")))
require.False(t, isBlockedIP(net.ParseIP("8.8.8.8")))
}


func Test_sourceMapsStoreImpl_BlockPrivateDownloadURL(t *testing.T) {
var (
logger = alloyutil.TestAlloyLogger(t).Slog()
httpClient = &mockHTTPClient{}
store = newSourceMapsStore(
logger,
SourceMapsArguments{Download: true, DownloadFromOrigins: []string{"*"}},
newSourceMapMetrics(prometheus.NewRegistry()),
httpClient,
newTestFileService(),
)
)
ex := &payload.Exception{Stacktrace: &payload.Stacktrace{Frames: []payload.Frame{{
Filename: "http://169.254.169.254/latest/meta-data/",
Lineno: 1,
Colno: 1,
Function: "x",
}}}}
actual := transformException(logger, store, ex, "1")
require.Equal(t, ex, actual)
require.Empty(t, httpClient.requests)
}