tenseleyflow/shithub / 337355f

Browse files

S35: signup throttle — add per-/24 layer (S35) atop per-IP (S05)

Authored by espadonne
SHA
337355faffd4611d57f1ab43c1660c1c2e9d3cd5
Parents
ec6208f
Tree
38e2b70

1 changed file

StatusFile+-
M internal/web/handlers/auth/auth.go 30 8
internal/web/handlers/auth/auth.gomodified
@@ -47,6 +47,7 @@ import (
4747
 	"github.com/tenseleyFlow/shithub/internal/auth/token"
4848
 	"github.com/tenseleyFlow/shithub/internal/infra/storage"
4949
 	"github.com/tenseleyFlow/shithub/internal/passwords"
50
+	"github.com/tenseleyFlow/shithub/internal/ratelimit"
5051
 	usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
5152
 	"github.com/tenseleyFlow/shithub/internal/web/middleware"
5253
 	"github.com/tenseleyFlow/shithub/internal/web/render"
@@ -58,14 +59,19 @@ var usernameRE = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$`)
5859
 // Deps is everything the auth handlers need. Constructed by the web
5960
 // package and injected at registration time.
6061
 type Deps struct {
61
-	Logger                   *slog.Logger
62
-	Render                   *render.Renderer
63
-	Pool                     *pgxpool.Pool
64
-	SessionStore             session.Store
65
-	Email                    email.Sender
66
-	Branding                 email.Branding
67
-	Argon2                   password.Params
68
-	Limiter                  *throttle.Limiter
62
+	Logger       *slog.Logger
63
+	Render       *render.Renderer
64
+	Pool         *pgxpool.Pool
65
+	SessionStore session.Store
66
+	Email        email.Sender
67
+	Branding     email.Branding
68
+	Argon2       password.Params
69
+	Limiter      *throttle.Limiter
70
+	// RateLimiter wraps the S35 generalised rate-limit table. Used
71
+	// for the per-/24 signup throttle (anti-abuse heuristic). nil
72
+	// disables the secondary throttle; the per-IP S05 limiter still
73
+	// applies.
74
+	RateLimiter              *ratelimit.Limiter
6975
 	RequireEmailVerification bool
7076
 	// SecretBox encrypts at-rest TOTP secrets. May be nil; when nil, the
7177
 	// 2FA enrollment endpoints are not registered.
@@ -770,12 +776,28 @@ func (h *Handlers) renderPage(w http.ResponseWriter, r *http.Request, page strin
770776
 }
771777
 
772778
 func (h *Handlers) throttleSignup(r *http.Request) error {
779
+	// Layer 1 — per-IP cap (S05). Tight per-host throttle so a single
780
+	// machine can't spin up dozens of accounts in an hour.
773781
 	if err := h.d.Limiter.Hit(r.Context(), h.d.Pool, throttle.Limit{
774782
 		Scope: "signup", Identifier: "ip:" + clientIP(r),
775783
 		Max: 5, Window: time.Hour,
776784
 	}); err != nil {
777785
 		return err
778786
 	}
787
+	// Layer 2 — per-/24 cap (S35 anti-abuse). Catches spray-from-many-
788
+	// IPs-on-the-same-network patterns. The threshold is intentionally
789
+	// looser (20/hour) than the per-IP cap so legitimate shared-NAT
790
+	// users (universities, corporate networks) aren't false-positives.
791
+	// nil RateLimiter skips this layer — the single-IP throttle still
792
+	// applies.
793
+	if h.d.RateLimiter != nil {
794
+		if ip, ok := ratelimit.ClientIP(r, false); ok {
795
+			d, err := h.d.RateLimiter.AllowSignupIP(r.Context(), ip, 20, time.Hour)
796
+			if err == nil && !d.Allowed {
797
+				return &throttle.ErrThrottled{RetryAfter: d.RetryAfter, Hits: d.Limit + 1}
798
+			}
799
+		}
800
+	}
779801
 	return nil
780802
 }
781803