towerops/lib/towerops_web/plugs/brute_force_protection.ex
Graham McIntie 1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia)
- Jason.decode! → Jason.decode with error handling for untrusted input
- inspect() leak: replace with generic error messages, log details server-side
- SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes
- SSRF validation added to HTTP monitoring executor and integration credentials
- GraphQL complexity limits: always applied, not just in prod
- GraphQL introspection: also check GET query params, not just body
- Stripe webhook: explicit nil/empty checks for signature and body
- Cookie security: secure flag for session (prod), http_only+secure for remember_me
- Honeybadger API key: read from env var with fallback
- String.to_integer → Integer.parse with fallback for URL params
- String.to_atom → whitelist map for HTTP methods
- Gaiia webhook: remove secret_len and expected signature from log
- Admin API: add rate limiting pipeline
- to_atom_keys: per-key fallback instead of all-or-nothing rescue
2026-03-14 14:48:59 -05:00

80 lines
2.3 KiB
Elixir

defmodule ToweropsWeb.Plugs.BruteForceProtection do
@moduledoc """
Plug that detects and blocks brute force scanning behavior.
## Detection Strategy
Tracks unique 404 paths per IP address using Redis. When an unauthenticated
IP hits 5+ unique 404s within 60 seconds, a ban is created/escalated.
## Ban Escalation
- 1st offense: 5-minute ban
- 2nd offense (within 30 days): 1-hour ban
- 3rd+ offense (within 30 days): Permanent ban + Cloudflare WAF push
## Exemptions
- Authenticated users (conn.assigns[:current_user] present)
- Agent WebSocket connections (/socket/agent - authenticated at channel join)
- Whitelisted IPs (checked via BruteForce context)
## Placement
Must be placed in the endpoint.ex pipeline AFTER:
- RemoteIpLogger (needs remote_ip in Logger metadata)
- Static file serving (no need to track static assets)
Must be placed BEFORE:
- Router (needs to intercept requests before routing)
"""
import Plug.Conn
alias Towerops.Security.BruteForce
alias ToweropsWeb.RemoteIp
require Logger
def init(opts), do: opts
def call(conn, _opts) do
ip_address = RemoteIp.from_conn(conn)
cond do
# Skip for authenticated users
conn.assigns[:current_user] != nil ->
conn
# Skip for agent WebSocket connections (authenticated at channel join)
String.starts_with?(conn.request_path, "/socket/agent") ->
conn
# Check if IP is whitelisted
BruteForce.check_whitelist(ip_address) ->
conn
# Check if IP is currently banned
true ->
case BruteForce.check_ban_status(ip_address) do
:allowed ->
# Register a callback to track 404s after the response is sent
register_before_send(conn, fn response_conn ->
if response_conn.status == 404 do
Towerops.Security.FourOhFourTracker.record_404(ip_address, conn.request_path)
end
response_conn
end)
{:blocked, banned_until} ->
Logger.warning("Blocked request from banned IP: #{ip_address}, banned until: #{inspect(banned_until)}")
conn
|> put_resp_content_type("text/plain")
|> send_resp(403, "Access denied")
|> halt()
end
end
end
end