From 68881b30f1390e470c4ede1ddcc59011f78f8e61 Mon Sep 17 00:00:00 2001 From: agatha Date: Sat, 9 May 2026 13:54:49 -0400 Subject: [PATCH] Ops: Add script to test lockout with spoofed X-Forwarded-For headers --- scripts/test_lockout.sh | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 scripts/test_lockout.sh diff --git a/scripts/test_lockout.sh b/scripts/test_lockout.sh new file mode 100644 index 0000000..56051d2 --- /dev/null +++ b/scripts/test_lockout.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Test reactbin's login rate limiter and demonstrate the XFF injection bypass. +# +# Phase 1: Send 6 bad login attempts in quick succession. +# Attempts 1-5 should return 401 (invalid credentials). +# Attempt 6 should return 429 (rate limited) — the limiter blocks after +# max_failures=5 within the window. +# +# Phase 2: Send a 7th bad attempt with a spoofed X-Forwarded-For header +# pointing at a different IP. If the lockout keys correctly on the trusted +# client IP, this should still return 429 (same client, still locked). +# If reactbin trusts client-supplied XFF blindly, this would return 401 +# instead — the spoof would make the request look like a different client +# that hasn't accumulated failures. +# +# Interpretation: +# - 429 on attempt 7 → lockout is correctly identifying the client +# - 401 on attempt 7 → XFF injection succeeded; server treated us as a +# new client because we set a fake XFF +# +# Note: this script is ONLY useful when run against the public origin path +# where XFF spoofing is potentially possible. It does not exercise the +# Cloudflare-proxied path because Cloudflare strips/replaces XFF before +# forwarding to origin. + +set -u + +URL="${URL:-https://reactbin.juggalol.com/api/v1/auth/token}" +SPOOFED_IP="${SPOOFED_IP:-198.51.100.99}" # TEST-NET-2, never routed +USERNAME="${USERNAME:-not-a-real-user}" +PASSWORD="${PASSWORD:-not-a-real-password}" + +# JSON body for a bad login. Username/password chosen to be obviously fake; +# adjust if your auth provider has its own validation that would 400 instead +# of 401 on these values. +BODY=$(printf '{"username":"%s","password":"%s"}' "$USERNAME" "$PASSWORD") + +echo "Target: $URL" +echo "Body: $BODY" +echo + +echo "=== Phase 1: 6 bad logins from real client IP ===" +for i in 1 2 3 4 5 6; do + code=$(curl -sS -o /dev/null -w '%{http_code}' \ + -X POST \ + -H 'Content-Type: application/json' \ + --data "$BODY" \ + "$URL") + echo "Attempt $i: HTTP $code" +done + +echo +echo "=== Phase 2: 7th attempt with spoofed X-Forwarded-For ===" +echo "Setting X-Forwarded-For: $SPOOFED_IP" +code=$(curl -sS -o /dev/null -w '%{http_code}' \ + -X POST \ + -H 'Content-Type: application/json' \ + -H "X-Forwarded-For: $SPOOFED_IP" \ + --data "$BODY" \ + "$URL") +echo "Attempt 7: HTTP $code" + +echo +echo "Interpretation:" +echo " Attempt 7 = 429 → lockout correctly tracks real client; XFF spoof ineffective" +echo " Attempt 7 = 401 → XFF spoof succeeded; server believed the fake client IP"