From 7e200e7c77e1f19441fb4c63c3b9d29199b1b31b Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:54:40 +0100 Subject: [PATCH] feat: support method filtering (#3) --- README.md | 25 ++++++++++++++++++--- pkg/rules/engine.go | 9 ++++++++ pkg/rules/engine_test.go | 48 ++++++++++++++++++++++++++++++++++++++++ pkg/schema/schema.go | 1 + pkg/server/server.go | 14 +++++++++--- 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5ab1407..e173886 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ - [Response](#response-1) - [Environment variables](#environment-variables) - [Manual testing](#manual-testing) - - [Missing `X-Forwarded-For` and `X-Forwarded-Host` headers](#missing-x-forwarded-for-and-x-forwarded-host-headers) + - [Missing `X-Forwarded-For` and `X-Forwarded-Host` and `X-Forwarded-Method` headers](#missing-x-forwarded-for-and-x-forwarded-host-and-x-forwarded-method-headers) - [Missing `X-Forwarded-Host` header](#missing-x-forwarded-host-header) - [Missing `X-Forwarded-For` header](#missing-x-forwarded-for-header) + - [Missing `X-Forwarded-Method` header](#missing-x-forwarded-method-header) - [Blocked country](#blocked-country) - [Request authorized](#request-authorized) - [Roadmap](#roadmap) @@ -31,6 +32,7 @@ based on: - Client's IP address - Client's ASN (Autonomous System Number) - Requested domain +- Requested method ## Configuration @@ -43,6 +45,7 @@ more of the following criteria: - `countries`: List of country codes (ISO 3166-1 alpha-2) - `domains`: List of domain names +- `methods`: List of HTTP methods - `networks`: List of IP ranges in CIDR notation - `autonomous_systems`: List of ASNs @@ -74,13 +77,17 @@ access_control: policy: deny # Allow access to example.com and example.org from clients in - # France (FR) and the United States (US). + # France (FR) and the United States (US) using the GET or POST HTTP + # methods. - domains: - example.com - example.org countries: - FR - US + methods: + - GET + - POST policy: allow ``` @@ -167,7 +174,7 @@ Start geoblock with the provided example configuration: GEOBLOCK_CONFIG=examples/config.yaml GEOBLOCK_PORT=8080 make run ``` -### Missing `X-Forwarded-For` and `X-Forwarded-Host` headers +### Missing `X-Forwarded-For` and `X-Forwarded-Host` and `X-Forwarded-Method` headers ```http GET http://localhost:8080/v1/forward-auth @@ -178,6 +185,7 @@ GET http://localhost:8080/v1/forward-auth ```http GET http://localhost:8080/v1/forward-auth X-Forwarded-For: 127.0.0.1 +X-Forwarded-Method: GET ``` ### Missing `X-Forwarded-For` header @@ -185,6 +193,15 @@ X-Forwarded-For: 127.0.0.1 ```http GET http://localhost:8080/v1/forward-auth X-Forwarded-Host: example.com +X-Forwarded-Method: GET +``` + +### Missing `X-Forwarded-Method` header + +```http +GET http://localhost:8080/v1/forward-auth +X-Forwarded-For: 8.8.8.8 +X-Forwarded-Host: example.com ``` ### Blocked country @@ -193,6 +210,7 @@ X-Forwarded-Host: example.com GET http://localhost:8080/v1/forward-auth X-Forwarded-For: 8.8.8.8 X-Forwarded-Host: example.com +X-Forwarded-Method: GET ``` ### Request authorized @@ -201,6 +219,7 @@ X-Forwarded-Host: example.com GET http://localhost:8080/v1/forward-auth X-Forwarded-For: 127.0.0.1 X-Forwarded-Host: example.com +X-Forwarded-Method: GET ``` ## Roadmap diff --git a/pkg/rules/engine.go b/pkg/rules/engine.go index 20cd422..44e3faf 100644 --- a/pkg/rules/engine.go +++ b/pkg/rules/engine.go @@ -26,6 +26,7 @@ func NewEngine(config *schema.AccessControl) *Engine { // Query represents a query to be checked by the access control engine. type Query struct { RequestedDomain string + RequestedMethod string SourceIP net.IP SourceCountry string SourceASN uint32 @@ -48,6 +49,14 @@ func ruleApplies(rule *schema.AccessControlRule, query *Query) bool { } } + if len(rule.Methods) > 0 { + if utils.None(rule.Methods, func(method string) bool { + return strings.EqualFold(method, query.RequestedMethod) + }) { + return false + } + } + if len(rule.Networks) > 0 { if utils.None(rule.Networks, func(network schema.CIDR) bool { return network.Contains(query.SourceIP) diff --git a/pkg/rules/engine_test.go b/pkg/rules/engine_test.go index e2e84f5..e9b5416 100644 --- a/pkg/rules/engine_test.go +++ b/pkg/rules/engine_test.go @@ -84,6 +84,54 @@ func TestEngine_Authorize(t *testing.T) { }, want: false, }, + { + name: "allow by method", + config: &schema.AccessControl{ + Rules: []schema.AccessControlRule{ + { + Methods: []string{"GET", "POST"}, + Policy: schema.PolicyAllow, + }, + }, + DefaultPolicy: schema.PolicyDeny, + }, + query: &Query{ + RequestedMethod: "POST", + }, + want: true, + }, + { + name: "deny by method", + config: &schema.AccessControl{ + Rules: []schema.AccessControlRule{ + { + Methods: []string{"GET", "POST"}, + Policy: schema.PolicyDeny, + }, + }, + DefaultPolicy: schema.PolicyAllow, + }, + query: &Query{ + RequestedMethod: "POST", + }, + want: false, + }, + { + name: "deny unknown method", + config: &schema.AccessControl{ + Rules: []schema.AccessControlRule{ + { + Methods: []string{"GET"}, + Policy: schema.PolicyAllow, + }, + }, + DefaultPolicy: schema.PolicyDeny, + }, + query: &Query{ + RequestedMethod: "POST", + }, + want: false, + }, { name: "allow by network", config: &schema.AccessControl{ diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 40b31dc..da12a15 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -34,6 +34,7 @@ type AccessControlRule struct { Policy string `yaml:"policy" validate:"required,oneof=allow deny"` Networks []CIDR `yaml:"networks,omitempty" validate:"dive,cidr"` Domains []string `yaml:"domains,omitempty" validate:"dive,fqdn"` + Methods []string `yaml:"methods,omitempty" validate:"dive,oneof=GET HEAD POST PUT DELETE PATCH"` Countries []string `yaml:"countries,omitempty" validate:"dive,iso3166_1_alpha2"` AutonomousSystems []uint32 `yaml:"autonomous_systems,omitempty" validate:"dive,numeric"` } diff --git a/pkg/server/server.go b/pkg/server/server.go index 5014b2e..f3fe6af 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -24,6 +24,7 @@ const ( // Fields used in the log messages. const ( FieldRequestedDomain = "requested_domain" + FieldRequestedMethod = "requested_method" FieldSourceIP = "source_ip" FieldSourceCountry = "source_country" FieldSourceASN = "source_asn" @@ -39,14 +40,18 @@ func getForwardAuth( resolver *database.Resolver, engine *rules.Engine, ) { - origin := request.Header.Get(HeaderXForwardedFor) - domain := request.Header.Get(HeaderXForwardedHost) + var ( + origin = request.Header.Get(HeaderXForwardedFor) + domain = request.Header.Get(HeaderXForwardedHost) + method = request.Header.Get(HeaderXForwardedMethod) + ) // Block the request if one or more of the required headers are missing. It // probably means that the request didn't come from the reverse proxy. - if origin == "" || domain == "" { + if origin == "" || domain == "" || method == "" { log.WithFields(log.Fields{ FieldRequestedDomain: domain, + FieldRequestedMethod: method, FieldSourceIP: origin, }).Error("Missing required headers") writer.WriteHeader(http.StatusForbidden) @@ -59,6 +64,7 @@ func getForwardAuth( if sourceIP == nil { log.WithFields(log.Fields{ FieldRequestedDomain: domain, + FieldRequestedMethod: method, FieldSourceIP: origin, }).Error("Invalid source IP") writer.WriteHeader(http.StatusForbidden) @@ -69,6 +75,7 @@ func getForwardAuth( query := &rules.Query{ RequestedDomain: domain, + RequestedMethod: method, SourceIP: sourceIP, SourceCountry: resolved.CountryCode, SourceASN: resolved.ASN, @@ -76,6 +83,7 @@ func getForwardAuth( logFields := log.Fields{ FieldRequestedDomain: domain, + FieldRequestedMethod: method, FieldSourceIP: sourceIP, FieldSourceCountry: resolved.CountryCode, FieldSourceASN: resolved.ASN,