mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-05-09 08:32:51 +00:00
0491f1fac2
* fix: patch GHSA-6wcg-mqvh-fcvg PR https://github.com/TecharoHQ/anubis/pull/1015 added the ability for reverse proxies using Anubis in subrequest auth mode to look at the path of a request as there are many rules in the wild that rely on checking the path. This is how access to things like robots.txt or anything in the .well-known directory is unaffected by Anubis. However this logic was also enabled for non-subrequest deployments of Anubis, meaning that a specially crafted request could include a /.well-known/ path in it and then get around Anubis with little effort. This fix gates the logic behind a new plumbed variable named subrequestMode that only fires when Anubis is running in subrequest auth mode. This properly contains that workaround so that the logic does not fire in most deployments. Signed-off-by: Xe Iaso <me@xeiaso.net> * chore: spelling Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net>
213 lines
6.5 KiB
Go
213 lines
6.5 KiB
Go
package lib
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/TecharoHQ/anubis"
|
|
"github.com/TecharoHQ/anubis/data"
|
|
"github.com/TecharoHQ/anubis/internal"
|
|
"github.com/TecharoHQ/anubis/internal/honeypot/naive"
|
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
|
"github.com/TecharoHQ/anubis/lib/config"
|
|
"github.com/TecharoHQ/anubis/lib/localization"
|
|
"github.com/TecharoHQ/anubis/lib/policy"
|
|
"github.com/TecharoHQ/anubis/web"
|
|
"github.com/TecharoHQ/anubis/xess"
|
|
"github.com/a-h/templ"
|
|
)
|
|
|
|
type Options struct {
|
|
Next http.Handler
|
|
Policy *policy.ParsedConfig
|
|
Target string
|
|
TargetHost string
|
|
TargetSNI string
|
|
TargetInsecureSkipVerify bool
|
|
CookieDynamicDomain bool
|
|
CookieDomain string
|
|
CookieExpiration time.Duration
|
|
CookiePartitioned bool
|
|
BasePrefix string
|
|
WebmasterEmail string
|
|
RedirectDomains []string
|
|
ED25519PrivateKey ed25519.PrivateKey
|
|
HS512Secret []byte
|
|
StripBasePrefix bool
|
|
OpenGraph config.OpenGraph
|
|
ServeRobotsTXT bool
|
|
CookieSecure bool
|
|
CookieSameSite http.SameSite
|
|
Logger *slog.Logger
|
|
LogLevel string
|
|
PublicUrl string
|
|
JWTRestrictionHeader string
|
|
DifficultyInJWT bool
|
|
}
|
|
|
|
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*policy.ParsedConfig, error) {
|
|
var fin io.ReadCloser
|
|
var err error
|
|
|
|
if fname != "" {
|
|
fin, err = os.Open(fname)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
|
}
|
|
} else {
|
|
fname = "(data)/botPolicies.yaml"
|
|
fin, err = data.BotPolicies.Open("botPolicies.yaml")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("[unexpected] can't parse builtin policy file %s: %w", fname, err)
|
|
}
|
|
}
|
|
|
|
defer func(fin io.ReadCloser) {
|
|
err := fin.Close()
|
|
if err != nil {
|
|
slog.Error("failed to close policy file", "file", fname, "err", err)
|
|
}
|
|
}(fin)
|
|
|
|
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel, subrequestMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
|
}
|
|
var validationErrs []error
|
|
|
|
for _, b := range anubisPolicy.Bots {
|
|
if _, ok := challenge.Get(b.Challenge.Algorithm); !ok {
|
|
validationErrs = append(validationErrs, fmt.Errorf("%w %s", policy.ErrChallengeRuleHasWrongAlgorithm, b.Challenge.Algorithm))
|
|
}
|
|
}
|
|
|
|
if len(validationErrs) != 0 {
|
|
return nil, fmt.Errorf("can't do final validation of Anubis config: %w", errors.Join(validationErrs...))
|
|
}
|
|
|
|
return anubisPolicy, err
|
|
}
|
|
|
|
func New(opts Options) (*Server, error) {
|
|
if opts.Logger == nil {
|
|
opts.Logger = slog.With("subsystem", "anubis")
|
|
}
|
|
|
|
if opts.ED25519PrivateKey == nil && opts.HS512Secret == nil {
|
|
opts.Logger.Debug("opts.PrivateKey not set, generating a new one")
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lib: can't generate private key: %v", err)
|
|
}
|
|
opts.ED25519PrivateKey = priv
|
|
}
|
|
|
|
anubis.BasePrefix = strings.TrimRight(opts.BasePrefix, "/")
|
|
anubis.PublicUrl = opts.PublicUrl
|
|
|
|
result := &Server{
|
|
next: opts.Next,
|
|
ed25519Priv: opts.ED25519PrivateKey,
|
|
hs512Secret: opts.HS512Secret,
|
|
policy: opts.Policy,
|
|
opts: opts,
|
|
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store, ogtags.TargetOptions{
|
|
Host: opts.TargetHost,
|
|
SNI: opts.TargetSNI,
|
|
InsecureSkipVerify: opts.TargetInsecureSkipVerify,
|
|
}),
|
|
store: opts.Policy.Store,
|
|
logger: opts.Logger,
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
xess.Mount(mux)
|
|
|
|
// Helper to add global prefix
|
|
registerWithPrefix := func(pattern string, handler http.Handler, method string) {
|
|
if method != "" {
|
|
method = method + " " // methods must end with a space to register with them
|
|
}
|
|
|
|
// Ensure there's no double slash when concatenating BasePrefix and pattern
|
|
basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
|
|
prefix := method + basePrefix
|
|
|
|
// If pattern doesn't start with a slash, add one
|
|
if !strings.HasPrefix(pattern, "/") {
|
|
pattern = "/" + pattern
|
|
}
|
|
|
|
mux.Handle(prefix+pattern, handler)
|
|
}
|
|
|
|
// Ensure there's no double slash when concatenating BasePrefix and StaticPath
|
|
stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
|
|
registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
|
|
|
|
if opts.ServeRobotsTXT {
|
|
registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
|
}), "GET")
|
|
registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
|
}), "GET")
|
|
}
|
|
|
|
if opts.Policy.Impressum != nil {
|
|
registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
templ.Handler(
|
|
web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)),
|
|
).ServeHTTP(w, r)
|
|
}), "GET")
|
|
}
|
|
|
|
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
|
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
|
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
|
|
|
mazeGen, err := naive.New(result.store, result.logger)
|
|
if err == nil {
|
|
registerWithPrefix(anubis.APIPrefix+"honeypot/{id}/{stage}", mazeGen, http.MethodGet)
|
|
|
|
opts.Policy.Bots = append(
|
|
opts.Policy.Bots,
|
|
policy.Bot{
|
|
Rules: mazeGen.CheckNetwork(),
|
|
Action: config.RuleWeigh,
|
|
Weight: &config.Weight{
|
|
Adjust: 30,
|
|
},
|
|
Name: "honeypot/network",
|
|
},
|
|
)
|
|
} else {
|
|
result.logger.Error("can't init honeypot subsystem", "err", err)
|
|
}
|
|
|
|
//goland:noinspection GoBoolExpressions
|
|
if anubis.Version == "devel" {
|
|
// make-challenge is only used in tests. Only enable while version is devel
|
|
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
|
}
|
|
|
|
for _, implKind := range challenge.Methods() {
|
|
impl, _ := challenge.Get(implKind)
|
|
impl.Setup(mux)
|
|
}
|
|
|
|
result.mux = mux
|
|
|
|
return result, nil
|
|
}
|