Skip to content

Commit

Permalink
add network blocking features to HTTP loader (#241)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasf authored Nov 14, 2022
1 parent 6979431 commit 4b2e292
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 5 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,14 @@ Usage of imagor:
HTTP Loader default scheme if not specified by image path. Set "nil" to disable default scheme. (default "https")
-http-loader-accept string
HTTP Loader set request Accept header and validate response Content-Type header (default "*/*")
-http-loader-block-link-local-networks
HTTP Loader Proxy rejects connections to link local network IP addresses.
-http-loader-block-loopback-networks
HTTP Loader Proxy rejects connections to loopback network IP addresses.
-http-loader-block-private-networks
HTTP Loader Proxy rejects connections to private network IP addresses.
-http-loader-block-networks string
HTTP Loader Proxy rejects connections to link local network IP addresses. This options takes a comma separated list of networks in CIDR notation e.g ::1/128,127.0.0.0/8.
-http-loader-disable
Disable HTTP Loader

Expand Down
34 changes: 34 additions & 0 deletions config/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package config

import (
"net"
"strings"
)

// CIDRSliceFlag is a flag type which support comma separated CIDR expressions.
type CIDRSliceFlag []*net.IPNet

func (s *CIDRSliceFlag) String() string {
var ss []string
for _, v := range *s {
ss = append(ss, v.String())
}
return strings.Join(ss, ",")
}

func (s *CIDRSliceFlag) Set(value string) error {
var res []*net.IPNet
for _, v := range strings.Split(value, ",") {
_, network, err := net.ParseCIDR(v)
if err != nil {
return err
}
res = append(res, network)
}
*s = res
return nil
}

func (c *CIDRSliceFlag) Get() any {
return c
}
23 changes: 23 additions & 0 deletions config/flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import (
"testing"

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

func TestCIDRSliceFlag(t *testing.T) {
t.Run("set and get", func(t *testing.T) {
var f CIDRSliceFlag
input := "127.0.0.0/12,200.100.0.0/28"
assert.NoError(t, f.Set(input))
assert.Equal(t, input, f.String())
assert.Equal(t, &f, f.Get())

})
t.Run("parse error", func(t *testing.T) {
var f CIDRSliceFlag
input := "127.0.0.0/12,200.100.0.0/28."
assert.Error(t, f.Set(input))
})
}
20 changes: 17 additions & 3 deletions config/httpconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package config

import (
"flag"
"net"

"github.com/cshum/imagor"
"github.com/cshum/imagor/loader/httploader"
"go.uber.org/zap"
Expand Down Expand Up @@ -29,11 +31,19 @@ func withHTTPLoader(fs *flag.FlagSet, cb func() (*zap.Logger, bool)) imagor.Opti
"HTTP Loader Proxy URLs. Enable HTTP Loader proxy only if this value present. Accept csv of proxy urls e.g. http://user:pass@host:port,http://user:pass@host:port")
httpLoaderProxyAllowedSources = fs.String("http-loader-proxy-allowed-sources", "",
"HTTP Loader Proxy allowed hosts that enable proxy transport, if proxy URLs are set. Accept csv wth glob pattern e.g. *.google.com,*.git.luolix.top.")
httpLoaderDisable = fs.Bool("http-loader-disable", false,
httpLoaderBlockLoopbackNetworks = fs.Bool("http-loader-block-loopback-networks", false,
"HTTP Loader Proxy rejects connections to loopback network IP addresses.")
httpLoaderBlockPrivateNetworks = fs.Bool("http-loader-block-private-networks", false,
"HTTP Loader Proxy rejects connections to private network IP addresses.")
httpLoaderBlockLinkLocalNetworks = fs.Bool("http-loader-block-link-local-networks", false,
"HTTP Loader Proxy rejects connections to link local network IP addresses.")
httpLoaderBlockNetworks []*net.IPNet
httpLoaderDisable = fs.Bool("http-loader-disable", false,
"Disable HTTP Loader")

_, _ = cb()
)
fs.Var((*CIDRSliceFlag)(&httpLoaderBlockNetworks), "http-loader-block-networks",
"HTTP Loader Proxy rejects connections to link local network IP addresses. This options takes a comma separated list of networks in CIDR notation e.g. ::1/128,127.0.0.0/8.")
_, _ = cb()
return func(app *imagor.Imagor) {
if !*httpLoaderDisable {
// fallback with HTTP Loader unless explicitly disabled
Expand All @@ -48,6 +58,10 @@ func withHTTPLoader(fs *flag.FlagSet, cb func() (*zap.Logger, bool)) imagor.Opti
httploader.WithInsecureSkipVerifyTransport(*httpLoaderInsecureSkipVerifyTransport),
httploader.WithDefaultScheme(*httpLoaderDefaultScheme),
httploader.WithProxyTransport(*httpLoaderProxyURLs, *httpLoaderProxyAllowedSources),
httploader.WithBlockLoopbackNetworks(*httpLoaderBlockLoopbackNetworks),
httploader.WithBlockPrivateNetworks(*httpLoaderBlockPrivateNetworks),
httploader.WithBlockLinkLocalNetworks(*httpLoaderBlockLinkLocalNetworks),
httploader.WithBlockNetworks(httpLoaderBlockNetworks...),
),
)
}
Expand Down
53 changes: 51 additions & 2 deletions loader/httploader/httploader.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"syscall"

"github.com/cshum/imagor"
)
Expand Down Expand Up @@ -40,17 +42,32 @@ type HTTPLoader struct {
// Can be overridden by ForwardHeaders and OverrideHeaders
UserAgent string

// BlockLoopbackNetworks rejects HTTP connections to loopback network IP addresses.
BlockLoopbackNetworks bool

// BlockPrivateNetworks rejects HTTP connections to private network IP addresses.
BlockPrivateNetworks bool

// BlockLinkLocalNetworks rejects HTTP connections to link local IP addresses.
BlockLinkLocalNetworks bool

// BlockNetworks rejects HTTP connections to a configurable list of networks.
BlockNetworks []*net.IPNet

accepts []string
}

func New(options ...Option) *HTTPLoader {
h := &HTTPLoader{
Transport: http.DefaultTransport.(*http.Transport).Clone(),
OverrideHeaders: map[string]string{},
DefaultScheme: "https",
Accept: "*/*",
UserAgent: fmt.Sprintf("imagor/%s", imagor.Version),
}
transport := http.DefaultTransport.(*http.Transport).Clone()
dialer := &net.Dialer{Control: h.DialControl}
transport.DialContext = dialer.DialContext
h.Transport = transport

for _, option := range options {
option(h)
Expand Down Expand Up @@ -118,7 +135,11 @@ func (h *HTTPLoader) Get(r *http.Request, image string) (*imagor.Blob, error) {
return imagor.NewBlob(func() (io.ReadCloser, int64, error) {
resp, err := client.Do(req)
if err != nil {
if idx := strings.Index(err.Error(), "dial tcp: "); idx > -1 {
if errors.Is(err, ErrUnauthorizedRequest) {
err = imagor.NewError(
fmt.Sprintf("%s: %s", err.Error(), image),
http.StatusForbidden)
} else if idx := strings.Index(err.Error(), "dial tcp: "); idx > -1 {
err = imagor.NewError(
fmt.Sprintf("%s: %s", err.Error()[idx:], image),
http.StatusNotFound)
Expand Down Expand Up @@ -179,3 +200,31 @@ func (h *HTTPLoader) checkRedirect(r *http.Request, via []*http.Request) error {
}
return nil
}

var ErrUnauthorizedRequest = errors.New("unauthorized request")

// DialControl implements a net.Dialer.Control function which is automatically used with the default http.Transport.
// If the transport is replaced using the WithTransport option it is up to that
// transport if the control function is used or not.
func (s *HTTPLoader) DialControl(network string, address string, conn syscall.RawConn) error {
host, _, err := net.SplitHostPort(address)
if err != nil {
return err
}
addr := net.ParseIP(host)
if s.BlockLoopbackNetworks && addr.IsLoopback() {
return ErrUnauthorizedRequest
}
if s.BlockLinkLocalNetworks && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) {
return ErrUnauthorizedRequest
}
if s.BlockPrivateNetworks && addr.IsPrivate() {
return ErrUnauthorizedRequest
}
for _, network := range s.BlockNetworks {
if network.Contains(addr) {
return ErrUnauthorizedRequest
}
}
return nil
}
88 changes: 88 additions & 0 deletions loader/httploader/httploader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"compress/gzip"
"io"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -189,6 +190,93 @@ func TestWithAllowedSourcesRedirect(t *testing.T) {

}

func TestBlockNetworks(t *testing.T) {

t.Run("block loopback", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("should have been blocked")
w.Header().Set("Content-Type", "image/jpeg")
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))

}))
defer ts.Close()
loader := New(
WithBlockLoopbackNetworks(true),
)
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
assert.NoError(t, err)
blob, err := loader.Get(req, ts.URL)

b, err := blob.ReadAll()
assert.Empty(t, b)
assert.ErrorContains(t, err, "unauthorized request")

})

t.Run("block network", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("should have been blocked")
w.Header().Set("Content-Type", "image/jpeg")
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))

}))
defer ts.Close()
var networks []*net.IPNet

for _, v := range []string{"::1/128", "127.0.0.0/8"} {
_, network, err := net.ParseCIDR(v)
assert.NoError(t,err)
networks = append(networks, network)
}
loader := New(
WithBlockNetworks(networks...),
)
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
assert.NoError(t, err)
blob, err := loader.Get(req, ts.URL)

b, err := blob.ReadAll()
assert.Empty(t, b)
assert.ErrorContains(t, err, "unauthorized request")
})

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("should have been blocked")
w.Header().Set("Content-Type", "image/jpeg")
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))

}))
defer ts.Close()

for _, v := range []struct {
name string
addr string
opt Option
}{
{
name: "link local",
addr: "169.254.5.8:2000",
opt: WithBlockLinkLocalNetworks(true),
},
{
name: "private",
addr: "10.0.4.3:1000",
opt: WithBlockPrivateNetworks(true),
},
} {
t.Run(v.name, func(t *testing.T) {
loader := New(
v.opt,
)
err := loader.DialControl("ipv4", v.addr, nil)
assert.ErrorContains(t, err, "unauthorized request")
})
}
}

func TestWithDefaultScheme(t *testing.T) {
trans := testTransport{
"https://foo.bar/baz": "baz",
Expand Down
31 changes: 31 additions & 0 deletions loader/httploader/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package httploader

import (
"crypto/tls"
"net"
"net/http"
"strings"
)
Expand Down Expand Up @@ -111,3 +112,33 @@ func WithDefaultScheme(scheme string) Option {
}
}
}

func WithBlockLoopbackNetworks(enabled bool) Option {
return func(h *HTTPLoader) {
if enabled {
h.BlockLoopbackNetworks = true
}
}
}

func WithBlockLinkLocalNetworks(enabled bool) Option {
return func(h *HTTPLoader) {
if enabled {
h.BlockLinkLocalNetworks = true
}
}
}

func WithBlockPrivateNetworks(enabled bool) Option {
return func(h *HTTPLoader) {
if enabled {
h.BlockPrivateNetworks = true
}
}
}

func WithBlockNetworks(networks ...*net.IPNet) Option {
return func(h *HTTPLoader) {
h.BlockNetworks = networks
}
}

0 comments on commit 4b2e292

Please sign in to comment.