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 HTTP port rules to HTTPFilter (#185) #210

Merged
merged 1 commit into from
Feb 18, 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
52 changes: 52 additions & 0 deletions rcap/http_rulefilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,36 @@ package rcap
import (
"net"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/pkg/errors"

"github.com/suborbital/reactr/util"
)

var (
ErrHttpDisallowed = errors.New("requests to insecure HTTP endpoints is disallowed")
ErrIPsDisallowed = errors.New("requests to IP addresses are disallowed")
ErrPrivateDisallowed = errors.New("requests to private IP address ranges are disallowed")
ErrDomainDisallowed = errors.New("requests to this domain are disallowed")
ErrPortDisallowed = errors.New("requests to this port are disallowed")
)

// HTTPRules is a set of rules that governs use of the HTTP capability
type HTTPRules struct {
AllowedDomains []string `json:"allowedDomains" yaml:"allowedDomains"`
BlockedDomains []string `json:"blockedDomains" yaml:"blockedDomains"`
AllowedPorts []int `json:"allowedPorts" yaml:"allowedPorts"`
BlockedPorts []int `json:"blockedPorts" yaml:"blockedPorts"`
AllowIPs bool `json:"allowIPs" yaml:"allowIPs"`
AllowPrivate bool `json:"allowPrivate" yaml:"allowPrivate"`
AllowHTTP bool `json:"allowHTTP" yaml:"allowHTTP"`
}

var standardPorts = []int{80, 443}

// requestIsAllowed returns a non-nil error if the provided request is not allowed to proceed
func (h HTTPRules) requestIsAllowed(req *http.Request) error {
// Hostname removes port numbers as well as IPv6 [ and ]
Expand All @@ -35,6 +44,11 @@ func (h HTTPRules) requestIsAllowed(req *http.Request) error {
}
}

// Evaluate port access rules
if err := h.portAllowed(req.URL); err != nil {
return err
}

// determine if the passed-in host is an IP address
isRawIP := net.ParseIP(req.URL.Hostname()) != nil
if !h.AllowIPs && isRawIP {
Expand Down Expand Up @@ -90,6 +104,44 @@ func (h HTTPRules) requestIsAllowed(req *http.Request) error {
return nil
}

// portAllowed evaluates port allowance rules
func (h HTTPRules) portAllowed(url *url.URL) error {
// Backward Compatibility:
// Allow all ports if no allow/block list has been configured
if len(h.AllowedPorts)+len(h.BlockedPorts) == 0 {
return nil
}

port, err := readPort(url)
if err != nil {
return ErrPortDisallowed
}

if util.ContainsInt(port, h.BlockedPorts) {
return ErrPortDisallowed
}

for _, p := range append(standardPorts, h.AllowedPorts...) {
if p == port {
return nil
}
}

return ErrPortDisallowed
}

// readPort returns normalized URL port
func readPort(url *url.URL) (int, error) {
if url.Port() == "" {
if url.Scheme == "https" {
return 443, nil
}
return 80, nil
}

return strconv.Atoi(url.Port())
}

// returns nil if the host does not resolve to an IP in a private range
// returns ErrPrivateDisallowed if it does
func resolvesToPrivate(host string) error {
Expand Down
83 changes: 83 additions & 0 deletions rcap/http_rulefilter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,89 @@ func TestBlockedDomains(t *testing.T) {
})
}

func TestAllowedPorts(t *testing.T) {
rnpridgeon marked this conversation as resolved.
Show resolved Hide resolved
rules := defaultHTTPRules()
rules.AllowedPorts = []int{8080}

t.Run("standard http port allowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)

if err := rules.requestIsAllowed(req); err != nil {
t.Error("error occurred, should not have:", err)
}
})

t.Run("standard https port allowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "https://example.com", nil)

if err := rules.requestIsAllowed(req); err != nil {
t.Error("error occurred, should not have:", err)
}
})

t.Run("port 8080 allowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)

if err := rules.requestIsAllowed(req); err != nil {
t.Error("error occurred, should not have:", err)
}
})

t.Run("port 8088 disallowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8088", nil)

if err := rules.requestIsAllowed(req); err == nil {
t.Error("error did not occur, should have")
}
})
}

func TestBlockedPorts(t *testing.T) {
rules := defaultHTTPRules()
rules.AllowedPorts = []int{8081, 8082}
rules.BlockedPorts = []int{80, 443, 8080, 8081}

t.Run("standard HTTP port disallowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)

if err := rules.requestIsAllowed(req); err == nil {
t.Error("error did not occur, should have")
}
})

t.Run("standard HTTPS port disallowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "https://example.com", nil)

if err := rules.requestIsAllowed(req); err == nil {
t.Error("error did not occur, should have")
}
})

t.Run("port 8080 disallowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)

if err := rules.requestIsAllowed(req); err == nil {
t.Error("error did not occur, should have")
}
})

t.Run("blocked list takes precedence over allow list", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8081", nil)

if err := rules.requestIsAllowed(req); err == nil {
t.Error("error did not occur, should have")
}
})

t.Run("port 8082 allowed", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8082", nil)

if err := rules.requestIsAllowed(req); err != nil {
t.Error("error occurred, should not have:", err)
}
})
}

func TestBlockedWithCNAME(t *testing.T) {
rules := defaultHTTPRules()
rules.BlockedDomains = []string{"hosting.gitbook.io"}
Expand Down
10 changes: 10 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ func GenerateResultID() string {

return id
}

// ContainsInt returns true if value present in int slice
func ContainsInt(value int, values []int) bool {
for _, p := range values {
if p == value {
return true
}
}
return false
}
12 changes: 12 additions & 0 deletions util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ func TestGenerateResultID(t *testing.T) {
t.Errorf("id has length %d, expected 24", len(id))
}
}

func TestContainsInt(t *testing.T) {
container := []int{1, 2, 3, 4}

if !ContainsInt(3, container) {
t.Errorf("expected value not found in container")
}

if ContainsInt(5, container) {
t.Errorf("should not have found value in container")
}
}