Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasf committed Nov 13, 2022
1 parent 6979431 commit 25a6876
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 2 deletions.
46 changes: 44 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 All @@ -24,7 +26,7 @@ type HTTPLoader struct {
OverrideHeaders map[string]string

// AllowedSources list of host names allowed to load from,
// supports glob patterns such as *.google.com
// supports glob patterns such as *.google.comw
AllowedSources []string

// Accept set request Accept and validate response Content-Type header
Expand All @@ -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 @@ -179,3 +196,28 @@ func (h *HTTPLoader) checkRedirect(r *http.Request, via []*http.Request) error {
}
return nil
}

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
}
90 changes: 90 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,95 @@ 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 _, cidr := range []string{"::1/128", "127.0.0.0/8"} {
_, network, err := net.ParseCIDR(cidr)
if err != nil {
t.Error(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 = append(h.BlockNetworks, networks...)
}
}

0 comments on commit 25a6876

Please sign in to comment.