mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-05-15 11:19:38 +00:00
276b537776
* fix(policy): correctly wire subrequest mode through CEL/path checkers Previously Anubis only checked for the X-Original-Url when using subrequest mode. This header is used by the example nginx config to pass the request path through from the original client request to Anubis in order to do path-based filtering. According to facts and circumstances, Traefik hardcodes its own headers[1]: ```text httpdebug-1 | GET /.within.website/x/cmd/anubis/api/check httpdebug-1 | X-Forwarded-Method: GET httpdebug-1 | X-Forwarded-Proto: http httpdebug-1 | X-Forwarded-Server: b9a5d299c929 httpdebug-1 | X-Forwarded-Port: 8080 httpdebug-1 | X-Forwarded-Uri: / httpdebug-1 | X-Real-Ip: 172.18.0.1 httpdebug-1 | Accept-Encoding: gzip httpdebug-1 | User-Agent: curl/8.20.0 httpdebug-1 | Accept: */* httpdebug-1 | X-Forwarded-For: 172.18.0.1 httpdebug-1 | X-Forwarded-Host: localhost:8080 ``` As a result, this means that path-based filtering did not work. This commit fixes this issue by amending how path based checking logic works: * For CEL based checks, this pipes through the `subrequestMode` flag from main and alters the behaviour if either `X-Original-Url` or `X-Forwarded-Url` are found. These values are currently hardcoded for convenience but probably need to be made configurable in the policy file at a future date. * For path-based checks, this uses the existing `subrequestMode` flag from main and adds `X-Forwarded-Url` to the list of headers it checks. A smoke test was added to make sure that traefik in this mode continues to work. Thank you https://github.com/flifloo for filing a detailed issue with the relevant configuration fragments. Those configuration fragments formed the core of this smoke test. [1]: https://doc.traefik.io/traefik/v3.4/middlewares/http/forwardauth/ Closes: https://github.com/TecharoHQ/anubis/issues/1628 Signed-off-by: Xe Iaso <me@xeiaso.net> Co-Authored-By: flifloo <flifloo@gmail.com> * chore: spelling Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net> Co-authored-by: flifloo <flifloo@gmail.com>
179 lines
4.0 KiB
Go
179 lines
4.0 KiB
Go
package policy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/netip"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/TecharoHQ/anubis/internal"
|
|
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
|
"github.com/gaissmai/bart"
|
|
)
|
|
|
|
var (
|
|
ErrMisconfiguration = errors.New("[unexpected] policy: administrator misconfiguration")
|
|
)
|
|
|
|
type RemoteAddrChecker struct {
|
|
prefixTable *bart.Lite
|
|
hash string
|
|
}
|
|
|
|
func NewRemoteAddrChecker(cidrs []string) (checker.Impl, error) {
|
|
table := new(bart.Lite)
|
|
|
|
for _, cidr := range cidrs {
|
|
prefix, err := netip.ParsePrefix(cidr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: range %s not parsing: %w", ErrMisconfiguration, cidr, err)
|
|
}
|
|
|
|
table.Insert(prefix)
|
|
}
|
|
|
|
return &RemoteAddrChecker{
|
|
prefixTable: table,
|
|
hash: internal.FastHash(strings.Join(cidrs, ",")),
|
|
}, nil
|
|
}
|
|
|
|
func (rac *RemoteAddrChecker) Check(r *http.Request) (bool, error) {
|
|
host := r.Header.Get("X-Real-Ip")
|
|
if host == "" {
|
|
return false, fmt.Errorf("%w: header X-Real-Ip is not set", ErrMisconfiguration)
|
|
}
|
|
|
|
addr, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%w: %s is not an IP address: %w", ErrMisconfiguration, host, err)
|
|
}
|
|
|
|
// Convert IPv4-mapped IPv6 addresses to IPv4
|
|
if addr.Is6() && addr.Is4In6() {
|
|
addr = addr.Unmap()
|
|
}
|
|
|
|
return rac.prefixTable.Contains(addr), nil
|
|
}
|
|
|
|
func (rac *RemoteAddrChecker) Hash() string {
|
|
return rac.hash
|
|
}
|
|
|
|
type HeaderMatchesChecker struct {
|
|
header string
|
|
regexp *regexp.Regexp
|
|
hash string
|
|
}
|
|
|
|
func NewUserAgentChecker(rexStr string) (checker.Impl, error) {
|
|
return NewHeaderMatchesChecker("User-Agent", rexStr)
|
|
}
|
|
|
|
func NewHeaderMatchesChecker(header, rexStr string) (checker.Impl, error) {
|
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
|
}
|
|
return &HeaderMatchesChecker{strings.TrimSpace(header), rex, internal.FastHash(header + ": " + rexStr)}, nil
|
|
}
|
|
|
|
func (hmc *HeaderMatchesChecker) Check(r *http.Request) (bool, error) {
|
|
if hmc.regexp.MatchString(r.Header.Get(hmc.header)) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (hmc *HeaderMatchesChecker) Hash() string {
|
|
return hmc.hash
|
|
}
|
|
|
|
type PathChecker struct {
|
|
regexp *regexp.Regexp
|
|
hash string
|
|
subRequestMode bool
|
|
}
|
|
|
|
func NewPathChecker(rexStr string, subrequestMode bool) (checker.Impl, error) {
|
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
|
}
|
|
return &PathChecker{rex, internal.FastHash(rexStr), subrequestMode}, nil
|
|
}
|
|
|
|
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
|
if pc.subRequestMode {
|
|
originalUrl := r.Header.Get("X-Original-URI")
|
|
if originalUrl == "" {
|
|
originalUrl = r.Header.Get("X-Forwarded-Uri")
|
|
}
|
|
if originalUrl != "" {
|
|
if pc.regexp.MatchString(originalUrl) {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if pc.regexp.MatchString(r.URL.Path) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (pc *PathChecker) Hash() string {
|
|
return pc.hash
|
|
}
|
|
|
|
func NewHeaderExistsChecker(key string) checker.Impl {
|
|
return headerExistsChecker{strings.TrimSpace(key)}
|
|
}
|
|
|
|
type headerExistsChecker struct {
|
|
header string
|
|
}
|
|
|
|
func (hec headerExistsChecker) Check(r *http.Request) (bool, error) {
|
|
if r.Header.Get(hec.header) != "" {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (hec headerExistsChecker) Hash() string {
|
|
return internal.FastHash(hec.header)
|
|
}
|
|
|
|
func NewHeadersChecker(headermap map[string]string) (checker.Impl, error) {
|
|
var result checker.List
|
|
var errs []error
|
|
|
|
for key, rexStr := range headermap {
|
|
if rexStr == ".*" {
|
|
result = append(result, headerExistsChecker{strings.TrimSpace(key)})
|
|
continue
|
|
}
|
|
|
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("while compiling header %s regex %s: %w", key, rexStr, err))
|
|
continue
|
|
}
|
|
|
|
result = append(result, &HeaderMatchesChecker{key, rex, internal.FastHash(key + ": " + rexStr)})
|
|
}
|
|
|
|
if len(errs) != 0 {
|
|
return nil, errors.Join(errs...)
|
|
}
|
|
|
|
return result, nil
|
|
}
|