Files
anubis-mirror/internal/honeypot/naive/naive.go
T
Xe Iaso 926f3d1d0e fix: small security fixes (#1651)
This is based on private evaluation of a prerelease security product.
I cannot comment further other than I am impressed by its output.

This commit is a squash of several commits. The impactful commits
have details underneath markdown heading twos.

## fix(metrics): don't expose pprof by default

pprof[1] is the Go standard library profiling toolkit. It is invaluable
for diagnosing how Go programs perform in the wild. However it also is
able to expose secret data set with command line flags. This is not
ideal and should be mitigated by correctly configured firewall rules. We
don't live in a world where people correctly configure firewall rules,
so we have to fix things for people. Welcome to 2026.

[1]: https://pkg.go.dev/runtime/pprof

Ref: AWOO-001

## fix(honeypot/naive): cap r9k delay to one second

Otherwise this can get unbounded, which can cause problems with lesser
HTTP proxies such as Apache.

Ref: AWOO-002

## fix(policy): mend an edge case with subrequest auth and query strings

This fixes an unlikely edge case where using subrequest auth and query
strings with path based filtering can cause reality to differ from
administrator intent. This effectively strips the query string from
subrequest auth checks. This deficiency should be fixed in the future.

Ref: AWOO-004

## fix(expressions): mend possible nil pointer deref edge case

If Anubis just started up, load averages may not be set in memory. This
can cause a nil pointer dereference which could fail requests with weird
errors until the async thread sets the load averages.

Ref: AWOO-005

## fix(lib): mend case where domainless redirects could allow cross-domain redirects

Ref: AWOO-009

## fix(expressions): validate randInt bounds before rand.IntN

Non-positive or platform-overflowing arguments to the CEL randInt
helper used to reach rand.IntN unchecked, surfacing a CEL evaluator
error during request processing when policies passed
attacker-influenced values (e.g. contentLength). Reject non-positive
bounds and detect int narrowing explicitly, returning a typed CEL
error in both cases.

Ref: AWOO-010

Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
2026-05-30 00:48:43 -04:00

201 lines
4.8 KiB
Go

package naive
import (
"context"
_ "embed"
"fmt"
"log/slog"
"math"
"math/rand/v2"
"net/http"
"net/netip"
"time"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/honeypot"
"github.com/TecharoHQ/anubis/lib/policy/checker"
"github.com/TecharoHQ/anubis/lib/store"
"github.com/a-h/templ"
"github.com/google/uuid"
"github.com/nikandfor/spintax"
)
//go:generate go tool github.com/a-h/templ/cmd/templ generate
// XXX(Xe): All of this was generated by ChatGPT, GLM 4.6, and GPT-OSS 120b. This is pseudoprofound bullshit in spintax[1] format so that the bullshit generator can emit plausibly human-authored text while being very computationally cheap.
//
// It feels somewhat poetic to use spammer technology in Anubis.
//
// [1]: https://outboundly.ai/blogs/what-is-spintax-and-how-to-use-it/
//
//go:embed spintext.txt
var spintext string
//go:embed titles.txt
var titles string
//go:embed affirmations.txt
var affirmations string
func New(st store.Interface, lg *slog.Logger) (*Impl, error) {
affirmation, err := spintax.Parse(affirmations)
if err != nil {
return nil, fmt.Errorf("can't parse affirmations: %w", err)
}
body, err := spintax.Parse(spintext)
if err != nil {
return nil, fmt.Errorf("can't parse bodies: %w", err)
}
title, err := spintax.Parse(titles)
if err != nil {
return nil, fmt.Errorf("can't parse titles: %w", err)
}
lg.Debug("initialized basic bullshit generator", "affirmations", affirmation.Count(), "bodies", body.Count(), "titles", title.Count())
return &Impl{
st: st,
infos: store.JSON[honeypot.Info]{Underlying: st, Prefix: "honeypot:info"},
uaWeight: store.JSON[int]{Underlying: st, Prefix: "honeypot:user-agent"},
networkWeight: store.JSON[int]{Underlying: st, Prefix: "honeypot:network"},
affirmation: affirmation,
body: body,
title: title,
lg: lg.With("component", "honeypot/naive"),
}, nil
}
type Impl struct {
st store.Interface
infos store.JSON[honeypot.Info]
uaWeight store.JSON[int]
networkWeight store.JSON[int]
lg *slog.Logger
affirmation, body, title spintax.Spintax
}
func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network))
result++
i.networkWeight.Set(ctx, internal.SHA256sum(network), result, time.Hour)
return result
}
func (i *Impl) CheckNetwork() checker.Impl {
return checker.Func(func(r *http.Request) (bool, error) {
realIP, _ := internal.RealIP(r)
if !realIP.IsValid() {
realIP = netip.MustParseAddr(r.Header.Get("X-Real-Ip"))
}
network, ok := internal.ClampIP(realIP)
if !ok {
return false, nil
}
result, _ := i.networkWeight.Get(r.Context(), internal.SHA256sum(network.String()))
if result >= 25 {
return true, nil
}
return false, nil
})
}
func (i *Impl) Hash() string {
return internal.SHA256sum("naive honeypot")
}
func (i *Impl) makeAffirmations() []string {
count := rand.IntN(5) + 1
var result []string
for range count {
result = append(result, i.affirmation.Spin())
}
return result
}
func (i *Impl) makeSpins() []string {
count := rand.IntN(5) + 1
var result []string
for range count {
result = append(result, i.body.Spin())
}
return result
}
func (i *Impl) makeTitle() string {
return i.title.Spin()
}
func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
lg := internal.GetRequestLogger(i.lg, r)
id := r.PathValue("id")
if id == "" {
id = uuid.NewString()
}
realIP, _ := internal.RealIP(r)
if !realIP.IsValid() {
realIP = netip.MustParseAddr(r.Header.Get("X-Real-Ip"))
}
network, ok := internal.ClampIP(realIP)
if !ok {
lg.Error("clampIP failed", "output", network, "ok", ok)
http.Error(w, "The cake is a lie", http.StatusTeapot)
return
}
networkCount := i.incrementNetwork(r.Context(), network.String())
stage := r.PathValue("stage")
if stage == "init" {
lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network)
} else {
switch {
case networkCount%256 == 0:
lg.Warn("found possible crawler", "id", id, "network", network, "userAgent", r.UserAgent())
}
}
millisecondAmount := min(math.Pow(float64(networkCount), 2), 1000)
time.Sleep(time.Duration(millisecondAmount) * time.Millisecond)
spins := i.makeSpins()
affirmations := i.makeAffirmations()
title := i.makeTitle()
var links []link
for _, affirmation := range affirmations {
links = append(links, link{
href: uuid.NewString(),
body: affirmation,
})
}
templ.Handler(
base(title, i.maze(spins, links)),
templ.WithStreaming(),
templ.WithStatus(http.StatusOK),
).ServeHTTP(w, r)
t1 := time.Since(t0)
honeypot.Timings.WithLabelValues("naive").Observe(float64(t1.Milliseconds()))
}
type link struct {
href string
body string
}