Skip to content

Commit

Permalink
[CRE-47] Add safeurl to protect against SSRF
Browse files Browse the repository at this point in the history
  • Loading branch information
cedric-cordenier committed Jan 9, 2025
1 parent 0fb7546 commit 504ed31
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 19 deletions.
1 change: 1 addition & 0 deletions core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ require (
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/doyensec/safeurl v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/doyensec/safeurl v0.2.1 h1:DY15JorEfQsnpBWhBkVQIkaif2jfxCC14PIuGDsjDVs=
github.com/doyensec/safeurl v0.2.1/go.mod h1:wzSXqC/6Z410qHz23jtBWT+wQ8yTxcY0p8bZH/4EZIg=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down
43 changes: 37 additions & 6 deletions core/services/gateway/network/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"time"

"github.com/doyensec/safeurl"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
)

Expand All @@ -19,6 +21,28 @@ type HTTPClient interface {
type HTTPClientConfig struct {
MaxResponseBytes uint32
DefaultTimeout time.Duration
BlockedIPs []string
BlockedIPsCIDR []string
AllowedPorts []int
AllowedSchemes []string
}

var (
defaultAllowedPorts = []int{80, 443}
defaultAllowedSchemes = []string{"http", "https"}
)

func (c *HTTPClientConfig) ApplyDefaults() {
if len(c.AllowedPorts) == 0 {
c.AllowedPorts = defaultAllowedPorts
}

if len(c.AllowedSchemes) == 0 {
c.AllowedSchemes = defaultAllowedSchemes
}

// safeurl automatically blocks internal IPs so no need
// to set defaults here.
}

type HTTPRequest struct {
Expand All @@ -35,21 +59,28 @@ type HTTPResponse struct {
}

type httpClient struct {
client *http.Client
client *safeurl.WrappedClient
config HTTPClientConfig
lggr logger.Logger
}

// NewHTTPClient creates a new NewHTTPClient
// As of now, the client does not support TLS configuration but may be extended in the future
func NewHTTPClient(config HTTPClientConfig, lggr logger.Logger) (HTTPClient, error) {
config.ApplyDefaults()
safeConfig := safeurl.
GetConfigBuilder().
SetTimeout(config.DefaultTimeout).
SetAllowedPorts(config.AllowedPorts...).
SetAllowedSchemes(config.AllowedSchemes...).
SetBlockedIPs(config.BlockedIPs...).
SetBlockedIPsCIDR(config.BlockedIPsCIDR...).
Build()

return &httpClient{
config: config,
client: &http.Client{
Timeout: config.DefaultTimeout,
Transport: http.DefaultTransport,
},
lggr: lggr,
client: safeurl.Client(safeConfig),
lggr: lggr,
}, nil
}

Expand Down
143 changes: 130 additions & 13 deletions core/services/gateway/network/httpclient_test.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
package network_test
package network

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"

"github.com/doyensec/safeurl"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/gateway/network"
)

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

// Setup the test environment
lggr := logger.Test(t)
config := network.HTTPClientConfig{
config := HTTPClientConfig{
MaxResponseBytes: 1024,
DefaultTimeout: 5 * time.Second,
}
client, err := network.NewHTTPClient(config, lggr)
require.NoError(t, err)

// Define test cases
tests := []struct {
name string
setupServer func() *httptest.Server
request network.HTTPRequest
request HTTPRequest
expectedError error
expectedResp *network.HTTPResponse
expectedResp *HTTPResponse
}{
{
name: "successful request",
Expand All @@ -42,15 +42,15 @@ func TestHTTPClient_Send(t *testing.T) {
require.NoError(t, err2)
}))
},
request: network.HTTPRequest{
request: HTTPRequest{
Method: "GET",
URL: "/",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: nil,
expectedResp: &network.HTTPResponse{
expectedResp: &HTTPResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{"Content-Length": "7"},
Body: []byte("success"),
Expand All @@ -66,7 +66,7 @@ func TestHTTPClient_Send(t *testing.T) {
require.NoError(t, err2)
}))
},
request: network.HTTPRequest{
request: HTTPRequest{
Method: "GET",
URL: "/",
Headers: map[string]string{},
Expand All @@ -85,15 +85,15 @@ func TestHTTPClient_Send(t *testing.T) {
require.NoError(t, err2)
}))
},
request: network.HTTPRequest{
request: HTTPRequest{
Method: "GET",
URL: "/",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: nil,
expectedResp: &network.HTTPResponse{
expectedResp: &HTTPResponse{
StatusCode: http.StatusInternalServerError,
Headers: map[string]string{"Content-Length": "5"},
Body: []byte("error"),
Expand All @@ -108,7 +108,7 @@ func TestHTTPClient_Send(t *testing.T) {
require.NoError(t, err2)
}))
},
request: network.HTTPRequest{
request: HTTPRequest{
Method: "GET",
URL: "/",
Headers: map[string]string{},
Expand All @@ -126,6 +126,26 @@ func TestHTTPClient_Send(t *testing.T) {
server := tt.setupServer()
defer server.Close()

u, err := url.Parse(server.URL)
require.NoError(t, err)

hostname, port := u.Hostname(), u.Port()
portInt, err := strconv.ParseInt(port, 10, 32)
require.NoError(t, err)

safeConfig := safeurl.
GetConfigBuilder().
SetTimeout(config.DefaultTimeout).
SetAllowedIPs(hostname).
SetAllowedPorts(int(portInt)).
Build()

client := &httpClient{
config: config,
client: safeurl.Client(safeConfig),
lggr: lggr,
}

tt.request.URL = server.URL + tt.request.URL

resp, err := client.Send(context.Background(), tt.request)
Expand All @@ -145,3 +165,100 @@ func TestHTTPClient_Send(t *testing.T) {
})
}
}

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

// Setup the test environment
lggr := logger.Test(t)
config := HTTPClientConfig{
MaxResponseBytes: 1024,
DefaultTimeout: 5 * time.Second,
}

client, err := NewHTTPClient(config, lggr)
require.NoError(t, err)

// Define test cases
tests := []struct {
name string
request HTTPRequest
expectedError string
}{
{
name: "blocked port",
request: HTTPRequest{
Method: "GET",
URL: "http://127.0.0.1:8080",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: "port: 8080 not found in allowlist",
},
{
name: "blocked scheme",
request: HTTPRequest{
Method: "GET",
URL: "file://127.0.0.1",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: "scheme: file not found in allowlist",
},
{
name: "explicitly blocked IP",
request: HTTPRequest{
Method: "GET",
URL: "http://169.254.0.1",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: "ip: 169.254.0.1 not found in allowlist",
},
{
name: "explicitly blocked IP - internal network",
request: HTTPRequest{
Method: "GET",
URL: "http://169.254.0.1/endpoint",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: "ip: 169.254.0.1 not found in allowlist",
},
{
name: "explicitly blocked IP - localhost",
request: HTTPRequest{
Method: "GET",
URL: "http://127.0.0.1/endpoint",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: "ip: 127.0.0.1 not found in allowlist",
},
{
name: "explicitly blocked IP - current network",
request: HTTPRequest{
Method: "GET",
URL: "http://0.0.0.0/endpoint",
Headers: map[string]string{},
Body: nil,
Timeout: 2 * time.Second,
},
expectedError: "ip: 0.0.0.0 not found in allowlist",
},
}

// Execute test cases
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := client.Send(context.Background(), tt.request)
require.Error(t, err)
require.ErrorContains(t, err, tt.expectedError)
})
}
}
1 change: 1 addition & 0 deletions deployment/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/doyensec/safeurl v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions deployment/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/doyensec/safeurl v0.2.1 h1:DY15JorEfQsnpBWhBkVQIkaif2jfxCC14PIuGDsjDVs=
github.com/doyensec/safeurl v0.2.1/go.mod h1:wzSXqC/6Z410qHz23jtBWT+wQ8yTxcY0p8bZH/4EZIg=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e
github.com/deckarep/golang-set/v2 v2.6.0
github.com/dominikbraun/graph v0.23.0
github.com/doyensec/safeurl v0.2.1
github.com/esote/minmaxheap v1.0.0
github.com/ethereum/go-ethereum v1.14.11
github.com/fatih/color v1.17.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/doyensec/safeurl v0.2.1 h1:DY15JorEfQsnpBWhBkVQIkaif2jfxCC14PIuGDsjDVs=
github.com/doyensec/safeurl v0.2.1/go.mod h1:wzSXqC/6Z410qHz23jtBWT+wQ8yTxcY0p8bZH/4EZIg=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down
1 change: 1 addition & 0 deletions integration-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/doyensec/safeurl v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions integration-tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/doyensec/safeurl v0.2.1 h1:DY15JorEfQsnpBWhBkVQIkaif2jfxCC14PIuGDsjDVs=
github.com/doyensec/safeurl v0.2.1/go.mod h1:wzSXqC/6Z410qHz23jtBWT+wQ8yTxcY0p8bZH/4EZIg=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down
1 change: 1 addition & 0 deletions integration-tests/load/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/doyensec/safeurl v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions integration-tests/load/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/doyensec/safeurl v0.2.1 h1:DY15JorEfQsnpBWhBkVQIkaif2jfxCC14PIuGDsjDVs=
github.com/doyensec/safeurl v0.2.1/go.mod h1:wzSXqC/6Z410qHz23jtBWT+wQ8yTxcY0p8bZH/4EZIg=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down

0 comments on commit 504ed31

Please sign in to comment.