Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add network blocking support in HTTP loader #241

Merged
merged 1 commit into from
Nov 14, 2022
Merged
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
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
thomasf marked this conversation as resolved.
Show resolved Hide resolved

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
}
}