feat: add CSRF protection, SSRF prevention, and login rate limiting #42

Open
clawbot wants to merge 1 commits from security/csrf-ssrf-ratelimit into main
Collaborator

Security Hardening

This PR implements three security hardening issues:

CSRF Protection (closes #35)

  • Session-based CSRF tokens with cryptographically random 256-bit generation
  • Constant-time token comparison to prevent timing attacks
  • CSRF middleware applied to /pages, /sources, /source, and /user routes
  • Hidden csrf_token field added to all 12+ POST forms in templates
  • Excluded from /webhook (inbound webhook POSTs) and /api (stateless API)

SSRF Prevention (closes #36)

  • ValidateTargetURL() blocks private/reserved IP ranges at target creation time
  • Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10, plus multicast, reserved, test-net, and CGN ranges
  • SSRF-safe HTTP transport with custom DialContext in the delivery engine for defense-in-depth (prevents DNS rebinding attacks)
  • Only http and https schemes allowed

Login Rate Limiting (closes #37)

  • Per-IP rate limiter using golang.org/x/time/rate
  • 5 attempts per minute per IP on POST /pages/login
  • GET requests (form rendering) pass through unaffected
  • Automatic cleanup of stale per-IP limiter entries every 5 minutes
  • X-Forwarded-For and X-Real-IP header support for reverse proxies

Files Changed

New files:

  • internal/middleware/csrf.go + tests — CSRF middleware
  • internal/middleware/ratelimit.go + tests — Login rate limiter
  • internal/delivery/ssrf.go + tests — SSRF validation + safe transport

Modified files:

  • internal/server/routes.go — Wire CSRF and rate limit middleware
  • internal/handlers/handlers.go — Inject CSRF token into template data
  • internal/handlers/source_management.go — SSRF validation on target creation
  • internal/delivery/engine.go — SSRF-safe HTTP transport for production
  • All form templates — Added hidden csrf_token fields
  • README.md — Updated Security section and TODO checklist

docker build . passes (lint + tests + build).

## Security Hardening This PR implements three security hardening issues: ### CSRF Protection (closes https://git.eeqj.de/sneak/webhooker/issues/35) - Session-based CSRF tokens with cryptographically random 256-bit generation - Constant-time token comparison to prevent timing attacks - CSRF middleware applied to `/pages`, `/sources`, `/source`, and `/user` routes - Hidden `csrf_token` field added to all 12+ POST forms in templates - Excluded from `/webhook` (inbound webhook POSTs) and `/api` (stateless API) ### SSRF Prevention (closes https://git.eeqj.de/sneak/webhooker/issues/36) - `ValidateTargetURL()` blocks private/reserved IP ranges at target creation time - Blocked ranges: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`, `fe80::/10`, plus multicast, reserved, test-net, and CGN ranges - SSRF-safe HTTP transport with custom `DialContext` in the delivery engine for defense-in-depth (prevents DNS rebinding attacks) - Only `http` and `https` schemes allowed ### Login Rate Limiting (closes https://git.eeqj.de/sneak/webhooker/issues/37) - Per-IP rate limiter using `golang.org/x/time/rate` - 5 attempts per minute per IP on `POST /pages/login` - GET requests (form rendering) pass through unaffected - Automatic cleanup of stale per-IP limiter entries every 5 minutes - `X-Forwarded-For` and `X-Real-IP` header support for reverse proxies ### Files Changed **New files:** - `internal/middleware/csrf.go` + tests — CSRF middleware - `internal/middleware/ratelimit.go` + tests — Login rate limiter - `internal/delivery/ssrf.go` + tests — SSRF validation + safe transport **Modified files:** - `internal/server/routes.go` — Wire CSRF and rate limit middleware - `internal/handlers/handlers.go` — Inject CSRF token into template data - `internal/handlers/source_management.go` — SSRF validation on target creation - `internal/delivery/engine.go` — SSRF-safe HTTP transport for production - All form templates — Added hidden `csrf_token` fields - `README.md` — Updated Security section and TODO checklist `docker build .` passes (lint + tests + build).
clawbot added 1 commit 2026-03-05 12:04:48 +01:00
feat: add CSRF protection, SSRF prevention, and login rate limiting
All checks were successful
check / check (push) Successful in 5s
19e7557e88
Security hardening implementing three issues:

CSRF Protection (#35):
- Session-based CSRF tokens with cryptographically random generation
- Constant-time token comparison to prevent timing attacks
- CSRF middleware applied to /pages, /sources, /source, and /user routes
- Hidden csrf_token field added to all 12+ POST forms in templates
- Excluded from /webhook (inbound) and /api (stateless) routes

SSRF Prevention (#36):
- ValidateTargetURL blocks private/reserved IP ranges at target creation
- Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12,
  192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10, plus
  multicast, reserved, test-net, and CGN ranges
- SSRF-safe HTTP transport with custom DialContext for defense-in-depth
  at delivery time (prevents DNS rebinding attacks)
- Only http/https schemes allowed

Login Rate Limiting (#37):
- Per-IP rate limiter using golang.org/x/time/rate
- 5 attempts per minute per IP on POST /pages/login
- GET requests (form rendering) pass through unaffected
- Automatic cleanup of stale per-IP limiter entries
- X-Forwarded-For and X-Real-IP header support for reverse proxies

Closes #35, closes #36, closes #37
clawbot added the botneeds-review labels 2026-03-05 12:05:03 +01:00
Author
Collaborator

PR ready for review. Implements three security hardening issues:

  • CSRF protection (#35): Session-based CSRF tokens on all 12+ POST forms. Applied to /pages, /sources, /source, /user routes. Excluded from /webhook and /api.
  • SSRF prevention (#36): URL validation blocking private/reserved IPs at target creation + SSRF-safe HTTP transport with custom DialContext for defense-in-depth at delivery time.
  • Login rate limiting (#37): Per-IP rate limiter (5 attempts/min) on POST /pages/login using golang.org/x/time/rate.

All existing tests pass. New tests added for all three features. docker build . passes (lint + tests + build).

PR ready for review. Implements three security hardening issues: - **CSRF protection** ([#35](https://git.eeqj.de/sneak/webhooker/issues/35)): Session-based CSRF tokens on all 12+ POST forms. Applied to `/pages`, `/sources`, `/source`, `/user` routes. Excluded from `/webhook` and `/api`. - **SSRF prevention** ([#36](https://git.eeqj.de/sneak/webhooker/issues/36)): URL validation blocking private/reserved IPs at target creation + SSRF-safe HTTP transport with custom DialContext for defense-in-depth at delivery time. - **Login rate limiting** ([#37](https://git.eeqj.de/sneak/webhooker/issues/37)): Per-IP rate limiter (5 attempts/min) on `POST /pages/login` using `golang.org/x/time/rate`. All existing tests pass. New tests added for all three features. `docker build .` passes (lint + tests + build). <!-- session: agent:sdlc-manager:subagent:817efd4a-60c7-494b-a108-720b69ef2c70 -->
clawbot removed the needs-review label 2026-03-05 12:05:54 +01:00
Author
Collaborator

Review PASS — PR #42 (CSRF + SSRF + Rate Limiting)

All three security issues verified against requirements. docker build . passes.

#35 — CSRF Protection

  • Token generation: 256-bit cryptographically random tokens via crypto/rand — correct
  • Constant-time comparison: Custom secureCompare using XOR accumulation — correct (could use crypto/subtle.ConstantTimeCompare but functionally equivalent)
  • All 12 POST forms have <input type="hidden" name="csrf_token">:
    • login.html (1 form)
    • navbar.html (2 forms — desktop + mobile logout)
    • sources_new.html (1 form)
    • source_edit.html (1 form)
    • source_detail.html (7 forms — delete, add entrypoint, toggle/delete entrypoint, add target, toggle/delete target)
  • Middleware applied to /pages, /sources, /source/{sourceID}, /user/{username} routes
  • Correctly excluded from /webhook/{uuid} (inbound webhook POSTs) and /api/v1 (stateless API)
  • Tests: GET sets token, POST with valid token passes, POST without/invalid token returns 403

#36 — SSRF Prevention

  • All required private ranges blocked: 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, 0.0.0.0/8, ::1/128, fc00::/7, fe80::/10
  • Additional ranges also blocked: 100.64/10 (CGN), 192.0.0/24, 192.0.2/24 (TEST-NET-1), 198.18/15, 198.51.100/24 (TEST-NET-2), 203.0.113/24 (TEST-NET-3), 224.0.0.0/4 (multicast), 240.0.0.0/4 (reserved)
  • Two-layer defense: ValidateTargetURL checks at target creation time; NewSSRFSafeTransport with custom DialContext validates resolved IPs at connection time (DNS rebinding defense)
  • Transport correctly integrated into delivery engine HTTP client
  • Tests cover blocked/allowed IPs, blocked/allowed URLs, invalid schemes, empty hosts

#37 — Login Rate Limiting

  • Per-IP limiting using golang.org/x/time/rate: 5 attempts per minute (rate = 5/60 ≈ 0.0833/sec, burst = 5) — arithmetic correct
  • POST only: GET requests pass through unaffected — verified in code and tests
  • X-Forwarded-For support: Extracts first IP from comma-separated chain, falls back to X-Real-IP then RemoteAddr
  • Stale entry cleanup: Background goroutine prunes entries older than 10 minutes every 5 minutes — prevents unbounded memory growth
  • Tests verify: GET unlimited, POST limited after burst, independent per-IP tracking

Integrity Checks

  • No existing tests weakened or removed
  • .golangci.yml unchanged
  • docker build . (which runs make check) passes
  • Branch is up to date with main (no rebase needed)
  • README.md properly updated with security documentation and TODO checkmarks
  • New dependency golang.org/x/time v0.14.0 properly added to go.mod/go.sum
## ✅ Review PASS — [PR #42](https://git.eeqj.de/sneak/webhooker/pulls/42) (CSRF + SSRF + Rate Limiting) All three security issues verified against requirements. `docker build .` passes. ### [#35](https://git.eeqj.de/sneak/webhooker/issues/35) — CSRF Protection ✅ - **Token generation**: 256-bit cryptographically random tokens via `crypto/rand` — correct - **Constant-time comparison**: Custom `secureCompare` using XOR accumulation — correct (could use `crypto/subtle.ConstantTimeCompare` but functionally equivalent) - **All 12 POST forms** have `<input type="hidden" name="csrf_token">`: - `login.html` (1 form) - `navbar.html` (2 forms — desktop + mobile logout) - `sources_new.html` (1 form) - `source_edit.html` (1 form) - `source_detail.html` (7 forms — delete, add entrypoint, toggle/delete entrypoint, add target, toggle/delete target) - **Middleware applied** to `/pages`, `/sources`, `/source/{sourceID}`, `/user/{username}` routes - **Correctly excluded** from `/webhook/{uuid}` (inbound webhook POSTs) and `/api/v1` (stateless API) - Tests: GET sets token, POST with valid token passes, POST without/invalid token returns 403 ### [#36](https://git.eeqj.de/sneak/webhooker/issues/36) — SSRF Prevention ✅ - **All required private ranges blocked**: 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, 0.0.0.0/8, ::1/128, fc00::/7, fe80::/10 - **Additional ranges** also blocked: 100.64/10 (CGN), 192.0.0/24, 192.0.2/24 (TEST-NET-1), 198.18/15, 198.51.100/24 (TEST-NET-2), 203.0.113/24 (TEST-NET-3), 224.0.0.0/4 (multicast), 240.0.0.0/4 (reserved) - **Two-layer defense**: `ValidateTargetURL` checks at target creation time; `NewSSRFSafeTransport` with custom `DialContext` validates resolved IPs at connection time (DNS rebinding defense) - Transport correctly integrated into delivery engine HTTP client - Tests cover blocked/allowed IPs, blocked/allowed URLs, invalid schemes, empty hosts ### [#37](https://git.eeqj.de/sneak/webhooker/issues/37) — Login Rate Limiting ✅ - **Per-IP limiting** using `golang.org/x/time/rate`: 5 attempts per minute (rate = 5/60 ≈ 0.0833/sec, burst = 5) — arithmetic correct - **POST only**: GET requests pass through unaffected — verified in code and tests - **X-Forwarded-For support**: Extracts first IP from comma-separated chain, falls back to X-Real-IP then RemoteAddr - **Stale entry cleanup**: Background goroutine prunes entries older than 10 minutes every 5 minutes — prevents unbounded memory growth - Tests verify: GET unlimited, POST limited after burst, independent per-IP tracking ### Integrity Checks ✅ - No existing tests weakened or removed - `.golangci.yml` unchanged - `docker build .` (which runs `make check`) passes - Branch is up to date with `main` (no rebase needed) - README.md properly updated with security documentation and TODO checkmarks - New dependency `golang.org/x/time v0.14.0` properly added to go.mod/go.sum <!-- session: agent:sdlc-manager:subagent:74181fb1-e6fa-4941-bf14-08b49ea9bae9 -->
clawbot added merge-ready and removed bot labels 2026-03-05 12:09:05 +01:00
sneak was assigned by clawbot 2026-03-05 12:09:06 +01:00
Owner

@clawbot pls fix conflicts

@clawbot pls fix conflicts
clawbot removed the merge-ready label 2026-03-05 12:35:14 +01:00
clawbot force-pushed security/csrf-ssrf-ratelimit from 19e7557e88 to 7f4c40caca 2026-03-05 12:38:27 +01:00 Compare
Author
Collaborator

Rebased onto main to resolve merge conflicts from PR #41 merge.

Conflict resolution:

  • internal/server/routes.go: Merged both middleware stacks — /pages route group now applies CSRF, MaxBodySize, and LoginRateLimit. /sources and /source/{sourceID} groups retain CSRF, RequireAuth, and MaxBodySize.
  • README.md: Combined security documentation from both PRs (security headers + body size limits from PR #41, CSRF/SSRF/rate-limiting from this PR). Updated package layout, security section, and TODO checklist.

All tests pass. Docker build succeeds.

Rebased onto `main` to resolve merge conflicts from [PR #41](https://git.eeqj.de/sneak/webhooker/pulls/41) merge. **Conflict resolution:** - `internal/server/routes.go`: Merged both middleware stacks — `/pages` route group now applies CSRF, MaxBodySize, and LoginRateLimit. `/sources` and `/source/{sourceID}` groups retain CSRF, RequireAuth, and MaxBodySize. - `README.md`: Combined security documentation from both PRs (security headers + body size limits from PR #41, CSRF/SSRF/rate-limiting from this PR). Updated package layout, security section, and TODO checklist. All tests pass. Docker build succeeds. <!-- session: agent:sdlc-manager:subagent:932b81e8-1544-4beb-940f-d9ef3d60d1e7 -->
clawbot added the bot label 2026-03-05 12:39:31 +01:00
Author
Collaborator

Post-Rebase Review PASS — PR #42

Verified all three security features are intact after rebase onto main (which merged PR #41: security headers, session fixation, body limits). docker build . passes.

Middleware Stack Verification

Both PR #41 and PR #42 middleware correctly present in routes.go:

Layer Middleware Source
Global SecurityHeaders PR #41
/pages CSRF → MaxBodySize PR #42 + PR #41
/pages login group LoginRateLimit PR #42
/user/{username} CSRF PR #42
/sources CSRF → RequireAuth → MaxBodySize PR #42 + PR #41
/source/{sourceID} CSRF → RequireAuth → MaxBodySize PR #42 + PR #41
/webhook/{uuid} None (correct — external webhook traffic)

No code lost or duplicated in conflict resolution.

#35 — CSRF Protection

  • 12 POST forms across 5 templates, all 12 have csrf_token hidden input
  • Session-based 256-bit tokens with constant-time comparison
  • Applied to /pages, /user, /sources, /source routes
  • Correctly excluded from /webhook (inbound) and /api (stateless)

#36 — SSRF Prevention

  • All required ranges blocked: 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1/128, fc00::/7, fe80::/10
  • Additional ranges: 0.0.0.0/8, 100.64/10 (CGN), multicast, reserved
  • DNS rebinding defense: NewSSRFSafeTransport() re-resolves hostnames at dial time and validates resolved IPs
  • Dual enforcement: URL validation at target creation (HandleTargetCreate) + transport-level blocking at delivery time (Engine.client.Transport)
  • Comprehensive tests: 142 lines covering all blocked/allowed ranges, edge cases

#37 — Login Rate Limiting

  • 5 attempts/minute per IP using golang.org/x/time/rate token bucket
  • POST-only (GET requests for login form pass through)
  • Per-IP limiter map with cleanup goroutine (10-min expiry, 5-min sweep)
  • IP extraction: X-Forwarded-For → X-Real-IP → RemoteAddr
  • Tests verify burst behavior, per-IP independence, GET bypass

Additional Checks

  • .golangci.yml: unchanged
  • No tests weakened
  • go.mod adds only golang.org/x/time (required for rate limiter)
  • README.md updated with security docs and TODO checkboxes

Minor Style Note (non-blocking)

secureCompare() in csrf.go is a hand-rolled constant-time comparison. Consider using crypto/subtle.ConstantTimeCompare from stdlib for the same guarantee with less code. Not a functional issue — the current implementation is correct.

## ✅ Post-Rebase Review PASS — [PR #42](https://git.eeqj.de/sneak/webhooker/pulls/42) Verified all three security features are intact after rebase onto main (which merged [PR #41](https://git.eeqj.de/sneak/webhooker/pulls/41): security headers, session fixation, body limits). `docker build .` passes. ### Middleware Stack Verification ✅ Both PR #41 and PR #42 middleware correctly present in `routes.go`: | Layer | Middleware | Source | |-------|-----------|--------| | Global | SecurityHeaders | PR #41 | | `/pages` | CSRF → MaxBodySize | PR #42 + PR #41 | | `/pages` login group | LoginRateLimit | PR #42 | | `/user/{username}` | CSRF | PR #42 | | `/sources` | CSRF → RequireAuth → MaxBodySize | PR #42 + PR #41 | | `/source/{sourceID}` | CSRF → RequireAuth → MaxBodySize | PR #42 + PR #41 | | `/webhook/{uuid}` | None (correct — external webhook traffic) | — | No code lost or duplicated in conflict resolution. ### [#35](https://git.eeqj.de/sneak/webhooker/issues/35) — CSRF Protection ✅ - 12 POST forms across 5 templates, all 12 have `csrf_token` hidden input - Session-based 256-bit tokens with constant-time comparison - Applied to `/pages`, `/user`, `/sources`, `/source` routes - Correctly excluded from `/webhook` (inbound) and `/api` (stateless) ### [#36](https://git.eeqj.de/sneak/webhooker/issues/36) — SSRF Prevention ✅ - All required ranges blocked: `127/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`, `::1/128`, `fc00::/7`, `fe80::/10` - Additional ranges: `0.0.0.0/8`, `100.64/10` (CGN), multicast, reserved - **DNS rebinding defense**: `NewSSRFSafeTransport()` re-resolves hostnames at dial time and validates resolved IPs - Dual enforcement: URL validation at target creation (`HandleTargetCreate`) + transport-level blocking at delivery time (`Engine.client.Transport`) - Comprehensive tests: 142 lines covering all blocked/allowed ranges, edge cases ### [#37](https://git.eeqj.de/sneak/webhooker/issues/37) — Login Rate Limiting ✅ - 5 attempts/minute per IP using `golang.org/x/time/rate` token bucket - POST-only (GET requests for login form pass through) - Per-IP limiter map with cleanup goroutine (10-min expiry, 5-min sweep) - IP extraction: X-Forwarded-For → X-Real-IP → RemoteAddr - Tests verify burst behavior, per-IP independence, GET bypass ### Additional Checks - `.golangci.yml`: unchanged ✅ - No tests weakened ✅ - `go.mod` adds only `golang.org/x/time` (required for rate limiter) ✅ - README.md updated with security docs and TODO checkboxes ✅ ### Minor Style Note (non-blocking) `secureCompare()` in `csrf.go` is a hand-rolled constant-time comparison. Consider using `crypto/subtle.ConstantTimeCompare` from stdlib for the same guarantee with less code. Not a functional issue — the current implementation is correct. <!-- session: agent:sdlc-manager:subagent:b50dd3c0-8dea-4aa2-8f82-045391d7c03a -->
clawbot added merge-ready and removed bot labels 2026-03-05 12:42:19 +01:00
All checks were successful
check / check (push) Successful in 4s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin security/csrf-ssrf-ratelimit:security/csrf-ssrf-ratelimit
git checkout security/csrf-ssrf-ratelimit
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#42