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
7 changes: 5 additions & 2 deletions server/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ type rpcProxyClient struct {

func NewRPCProxyClient(logger log.Logger, proxyURL string, timeoutSeconds int, fingerprint Fingerprint) RPCProxyClient {
return &rpcProxyClient{
logger: logger,
httpClient: http.Client{Timeout: time.Second * time.Duration(timeoutSeconds)},
logger: logger,
httpClient: http.Client{
Timeout: time.Second * time.Duration(timeoutSeconds),
CheckRedirect: validateRedirectURL,
},
proxyURL: proxyURL,
fingerprint: fingerprint,
}
Expand Down
103 changes: 103 additions & 0 deletions server/outbound_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package server

import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
)

// Blocked outbound ranges for user-supplied proxy / mempool RPC URLs.
// Broader than XFF fingerprint private ranges (includes CGNAT / unspecified).
var blockedOutboundCidrs []*net.IPNet

func init() {
cidrs := []string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.168.0.0/16",
"::/128",
"::1/128",
"fc00::/7",
"fe80::/10",
}
blockedOutboundCidrs = make([]*net.IPNet, 0, len(cidrs))
for _, c := range cidrs {
_, network, err := net.ParseCIDR(c)
if err != nil {
panic(err)
}
blockedOutboundCidrs = append(blockedOutboundCidrs, network)
}
}

func isBlockedOutboundIP(ip net.IP) bool {
if ip == nil {
return true
}
for _, network := range blockedOutboundCidrs {
if network.Contains(ip) {
return true
}
}
return false
}

// ValidateOutboundRPCURL ensures a user-supplied RPC proxy URL uses http(s)
// and does not resolve to a private / link-local / loopback address.
func ValidateOutboundRPCURL(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return fmt.Errorf("empty url")
}
parsed, err := url.Parse(raw)
if err != nil {
return fmt.Errorf("invalid url: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return fmt.Errorf("url scheme must be http or https")
}
host := parsed.Hostname()
if host == "" {
return fmt.Errorf("url missing host")
}
if strings.EqualFold(host, "localhost") {
return fmt.Errorf("private or link-local address not allowed")
}

if ip := net.ParseIP(host); ip != nil {
if isBlockedOutboundIP(ip) {
return fmt.Errorf("private or link-local address not allowed")
}
return nil
}

ips, err := net.LookupIP(host)
if err != nil {
return fmt.Errorf("failed to resolve host: %w", err)
}
if len(ips) == 0 {
return fmt.Errorf("failed to resolve host")
}
for _, ip := range ips {
if isBlockedOutboundIP(ip) {
return fmt.Errorf("private or link-local address not allowed")
}
}
return nil
}

func validateRedirectURL(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("stopped after 10 redirects")
}
if req == nil || req.URL == nil {
return fmt.Errorf("invalid redirect")
}
return ValidateOutboundRPCURL(req.URL.String())
}
44 changes: 44 additions & 0 deletions server/outbound_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package server

import (
"net/http"
"net/url"
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateOutboundRPCURL(t *testing.T) {
t.Parallel()

require.NoError(t, ValidateOutboundRPCURL("https://example.com"))
require.NoError(t, ValidateOutboundRPCURL("http://1.1.1.1:8545"))

require.Error(t, ValidateOutboundRPCURL("http://127.0.0.1/"))
require.Error(t, ValidateOutboundRPCURL("http://169.254.169.254/latest/meta-data/"))
require.Error(t, ValidateOutboundRPCURL("http://10.0.0.5"))
require.Error(t, ValidateOutboundRPCURL("http://192.168.1.1"))
require.Error(t, ValidateOutboundRPCURL("http://172.16.0.1"))
require.Error(t, ValidateOutboundRPCURL("http://100.64.0.1"))
require.Error(t, ValidateOutboundRPCURL("http://localhost:8545"))
require.Error(t, ValidateOutboundRPCURL("file:///etc/passwd"))
require.Error(t, ValidateOutboundRPCURL("ftp://example.com"))
require.Error(t, ValidateOutboundRPCURL("not a url"))
}

func TestValidateRedirectURL(t *testing.T) {
t.Parallel()

req := &http.Request{URL: mustURL(t, "http://127.0.0.1/")}
require.Error(t, validateRedirectURL(req, nil))

req = &http.Request{URL: mustURL(t, "https://example.com/rpc")}
require.NoError(t, validateRedirectURL(req, nil))
}

func mustURL(t *testing.T, raw string) *url.URL {
t.Helper()
u, err := url.Parse(raw)
require.NoError(t, err)
return u
}
8 changes: 7 additions & 1 deletion server/request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,15 @@ func (r *RpcRequestHandler) process() {
referer := r.req.Header.Get("Referer")

// If users specify a proxy url in their rpc endpoint they can have their requests proxied to that endpoint instead of Infura
// e.g. https://rpc.flashbots.net?url=http://RPC-ENDPOINT.COM
// e.g. https://rpc.flashbots.net?url=https://RPC-ENDPOINT.COM
customProxyUrl, ok := r.req.URL.Query()["url"]
if ok && len(customProxyUrl[0]) > 1 {
if err := ValidateOutboundRPCURL(customProxyUrl[0]); err != nil {
r.requestRecord.UpdateRequestEntry(r.req, http.StatusBadRequest, err.Error())
r.logger.Warn("[process] Rejected custom url", "url", customProxyUrl[0], "error", err)
(*r.respw).WriteHeader(http.StatusBadRequest)
return
}
metrics.UrlParamUsageInc()
r.defaultProxyUrl = customProxyUrl[0]
r.logger.Info("[process] Using custom url", "url", r.defaultProxyUrl)
Expand Down
2 changes: 1 addition & 1 deletion server/request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func TestGetEffectiveParametersHeaderNoPreset(t *testing.T) {

func TestRpcRequestHandler_UrlParam(t *testing.T) {
wrec := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/?url=http://mock.url", nil)
req := httptest.NewRequest("POST", "/?url=https://example.com", nil)

metrics.UrlParamUsage.Set(0)

Expand Down
3 changes: 3 additions & 0 deletions server/url_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ func ExtractParametersFromUrl(reqUrl *url.URL, allBuilders []string) (params URL
return params, ErrIncorrectMempoolURL
}
parsedUrl.Scheme = "https"
if err := ValidateOutboundRPCURL(parsedUrl.String()); err != nil {
return params, ErrIncorrectMempoolURL
}
params.pref.Privacy.MempoolRPC = parsedUrl.String()
}
blockRange := normalizedQuery["blockrange"]
Expand Down