use x-forwarded-for to rate limit login attempts by ip

This commit is contained in:
Blake Blackshear 2024-05-15 06:05:33 -05:00
parent 6d6a54c5ae
commit 35f02bd505
5 changed files with 53 additions and 5 deletions

View File

@ -7,7 +7,7 @@ proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-URI $request_uri;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
## Basic Proxy Configuration

View File

@ -87,8 +87,9 @@ def create_app(
app.camera_error_image = None
app.stats_emitter = stats_emitter
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
# initialize the rate limiter for the login endpoint
# update the request_address with the x-forwarded-for header from nginx
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
# initialize the rate limiter for the login endpoint
limiter.init_app(app)
if frigate_config.auth.failed_login_rate_limit is None:
limiter.enabled = False

View File

@ -2,6 +2,7 @@
import base64
import hashlib
import ipaddress
import json
import logging
import os
@ -13,7 +14,6 @@ from pathlib import Path
from flask import Blueprint, current_app, jsonify, make_response, request
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from joserfc import jwt
from peewee import DoesNotExist
@ -24,8 +24,51 @@ logger = logging.getLogger(__name__)
AuthBp = Blueprint("auth", __name__)
def get_remote_addr():
route = list(reversed(request.headers.get("x-forwarded-for").split(",")))
logger.debug(f"IP Route: {[r for r in route]}")
trusted_proxies = []
for proxy in current_app.frigate_config.auth.trusted_proxies:
try:
network = ipaddress.ip_network(proxy)
except ValueError:
logger.warn(f"Unable to parse trusted network: {proxy}")
trusted_proxies.append(network)
# return the first remote address that is not trusted
for addr in route:
ip = ipaddress.ip_address(addr.strip())
logger.debug(f"Checking {ip} (v{ip.version})")
trusted = False
for trusted_proxy in trusted_proxies:
logger.debug(
f"Checking against trusted proxy: {trusted_proxy} (v{trusted_proxy.version})"
)
if trusted_proxy.version == 4:
ipv4 = ip.ipv4_mapped if ip.version == 6 else ip
if ipv4 in trusted_proxy:
trusted = True
logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}")
break
elif trusted_proxy.version == 6 and ip.version == 6:
if ip in trusted_proxy:
trusted = True
logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}")
break
if trusted:
logger.debug(f"{ip} is trusted")
continue
else:
logger.debug(f"First untrusted IP: {str(ip)}")
return str(ip)
# if there wasn't anything in the route, just return the default
return request.remote_addr or "127.0.0.1"
limiter = Limiter(
lambda: get_remote_address,
get_remote_addr,
storage_uri="memory://",
)

View File

@ -136,6 +136,10 @@ class AuthConfig(FrigateBaseModel):
default="1/second;5/minute;20/hour",
title="Rate limits for failed login attempts.",
)
trusted_proxies: Optional[List[str]] = Field(
default=[],
title="Trusted proxies for determining IP address to rate limit",
)
# As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256
hash_iterations: int = Field(default=600000, title="Password hash iterations")

View File

@ -59,7 +59,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
if (err.response?.status === 429) {
toast.error("Exceeded rate limit. Try again in 1 minute.", {
toast.error("Exceeded rate limit. Try again later.", {
position: "top-center",
});
} else if (err.response?.status === 400) {