Skip to content

Commit

Permalink
add network blocking features to HTTP loader
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasf committed Nov 13, 2022
1 parent 6979431 commit c716257
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 1 deletion.
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
13 changes: 13 additions & 0 deletions config/httpconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"flag"

"github.com/cshum/imagor"
"github.com/cshum/imagor/loader/httploader"
"go.uber.org/zap"
Expand Down Expand Up @@ -29,6 +30,14 @@ 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.")
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 = fs.String("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.")
httpLoaderDisable = fs.Bool("http-loader-disable", false,
"Disable HTTP Loader")

Expand All @@ -48,6 +57,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
56 changes: 55 additions & 1 deletion 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,33 @@ 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 string
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 All @@ -65,6 +83,14 @@ func New(options ...Option) *HTTPLoader {
}
}
}
if h.BlockNetworks != "" {
for _, s := range strings.Split(h.BlockNetworks, ",") {
_, network, err := net.ParseCIDR(s) // TODO: log error? abort program?
if err == nil {
h.blockNetworks = append(h.blockNetworks, network)
}
}
}
return h
}

Expand Down Expand Up @@ -179,3 +205,31 @@ func (h *HTTPLoader) checkRedirect(r *http.Request, via []*http.Request) error {
}
return nil
}

// 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 fmt.Errorf("unauthorized request")
}
if s.BlockLinkLocalNetworks && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) {
return fmt.Errorf("unauthorized request")
}
if s.BlockPrivateNetworks && addr.IsPrivate() {
return fmt.Errorf("unauthorized request")
}

for _, block := range s.blockNetworks {
if block.Contains(addr) {
return fmt.Errorf("unauthorized request")
}
}
return nil
}
80 changes: 80 additions & 0 deletions loader/httploader/httploader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,86 @@ 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()
loader := New(
WithBlockNetworks("::1/128,127.0.0.0/8"),
)
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
30 changes: 30 additions & 0 deletions loader/httploader/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,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 string) Option {
return func(h *HTTPLoader) {
h.BlockNetworks = networks
}
}

0 comments on commit c716257

Please sign in to comment.