From f5a5d63eb7101055d36aa0fc7c5293e76dbe8791 Mon Sep 17 00:00:00 2001 From: Andrew Stowell Date: Wed, 3 Dec 2025 11:05:32 -0500 Subject: [PATCH] finished adding rate limiting --- Dockerfile | 4 ++-- compose.yaml | 4 ++-- init.sh | 5 +++++ preauth-php.ini | 23 ++++++++++++----------- src/Auth.php | 27 ++++++++++++++++++++++----- template.php | 7 ++++--- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index 336112a..2219639 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ RUN mkdir -p /usr/local/etc/php/conf.d COPY preauth-php.ini /usr/local/etc/php/conf.d/preauth-php.ini # login sessions are stored here -RUN mkdir -p /tmp/sessions +RUN mkdir -p /tmp/data/sessions # rate limit monitoring information is stored here -RUN mkdir -p /tmp/monitor +RUN mkdir -p /tmp/data/monitor # fcgi command for the healthcheck RUN apk add fcgi diff --git a/compose.yaml b/compose.yaml index 4771977..08b165e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -12,8 +12,8 @@ services: restart: unless-stopped user: ${USER_ID}:${GROUP_ID} volumes: - - preauth-sessions:/tmp/sessions + - preauth:/tmp/data volumes: - preauth-sessions: + preauth: diff --git a/init.sh b/init.sh index efa3b65..3321885 100755 --- a/init.sh +++ b/init.sh @@ -1,5 +1,9 @@ #!/bin/sh +# ensure we have required folders +`mkdir -p /tmp/data/sessions` +`mkdir -p /tmp/data/monitor` + # ensure we have required settings if [ -z "$PREAUTH_KEY" ]; then export PREAUTH_KEY=$(cd /preauth && php init-key.php) @@ -9,5 +13,6 @@ if [ -z "$PREAUTH_TOKEN" ]; then export PREAUTH_TOKEN=$(cd /preauth && php init-token.php) fi +# start the process, now that setup is done exec php-fpm diff --git a/preauth-php.ini b/preauth-php.ini index 198a42e..73ad6f6 100644 --- a/preauth-php.ini +++ b/preauth-php.ini @@ -1,20 +1,21 @@ -# preauth uses "sessions" to store usage statistics by IP address and is used for rate limiting -# login sessions are stored as file in /tmp/sessions, and are unrelated to this configuration +; preauth uses php sessions to store usage statistics by IP address and is used for rate limiting +; login sessions are stored as file in /tmp/data/sessions, and are unrelated to this configuration -# sessions are IP based, cookie not needed nor wanted +; sessions are IP based, cookie not needed nor wanted session.use_cookies = off -session.use_only_cookies = off +; deprecated in php 8.4 +;session.use_only_cookies = off session.cache_limiter = '' session.name = preauth -session.save_path = /tmp/monitor +session.save_path = /tmp/data/monitor -# seconds until a session may be pruned -# IE: maximum time range to review for rate limiting and -# maximum time an IP address can be blocked -# 86400 seconds (24 hours) hardcoded upper limit +; seconds until a session may be pruned +; IE: maximum time range to review for rate limiting and +; maximum time an IP address can be blocked +; 86400 seconds aka 24 hours hardcoded upper limit session.gc_maxlifetime = 86400 -# disable random random garbage collection -# our healthcheck runs session_gc() +; disable random random garbage collection +; our healthcheck runs session_gc function session.gc_probability = 0 diff --git a/src/Auth.php b/src/Auth.php index c4d4434..b9acdb6 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -21,7 +21,7 @@ class Auth { /* cookie name */ private const NAME = '_auth_uuid'; /* directory to store sessions in */ - private const BASE = '/tmp/sessions/'; + private const BASE = '/tmp/data/sessions/'; /* top-level-domains which are known to have multiple parts */ private const TLD = [ 'ai' => ['com','net','off','org'], @@ -137,12 +137,12 @@ class Auth { } else { /* not already logged in, not trying to login, and not on auth page */ /* so send to login screen */ - $rt = rawurlencode( + $query = self::RETURN_FIELD . '=' . rawurlencode( ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'https') . '://' . ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $this->domain) . ($_SERVER['HTTP_X_FORWARDED_URI'] ?? '/') ); - header("Location: https://$this->preauth.$this->domain/?rt=$rt"); + header("Location: https://$this->preauth.$this->domain/?$query"); } } @@ -211,13 +211,15 @@ class Auth { private function rateLimit(): bool { /* our identifier for this remote-host, replace all special characters with dashes */ /* session_id() only allows ",", "-", and alphanumeric characters */ - $limiterId = preg_replace('[^a-zA-Z0-9]', '-', $this->getRemoteHost()); + $limiterId = preg_replace('/[^a-zA-Z0-9]/', '-', $this->getRemoteHost()); + //error_log("limiterId: {$limiterId}"); session_id($limiterId); session_start(); /* new remote-ip, start logging */ if ( ! isset($_SESSION['count'], $_SESSION['time'])) { $this->resetRateLimit(false); + //error_log('limiter new, allow'); return false; } @@ -230,14 +232,17 @@ class Auth { if ($sessionCount >= $rateLimit && time() - $rateBlocked <= $sessionTime) { /* if over limit, and block current, block them */ + //error_log("limiter over limit, block ($sessionCount)"); return true; } else if ($sessionCount < $rateLimit && time() - $rateTimeout <= $sessionTime) { /* if under limit, and timeout current, allow them */ + //error_log("limiter under limit, allow ($sessionCount)"); return false; } /* either over limit and block has expired or */ /* under limit and timeout has expired, so reset them */ + //error_log("limiter expired, reset"); $this->resetRateLimit(false); return false; } @@ -265,6 +270,7 @@ class Auth { $id = substr(preg_replace(self::URL64, '', $this->get[self::ID_FIELD]), 0, 100); $otp = TOTP::createFromSecret($this->token); $date = date('Y-m-d H:i:s'); + $remoteHost = $this->getRemoteHost(); /* if given token is valid, login, store session file and set the cookie */ if ($otp->now() === $this->get[self::TOKEN_FIELD]) { @@ -276,7 +282,7 @@ class Auth { file_put_contents( self::BASE . $encUUID, base64_encode($rawIV) . - "\$$id\$$this->expire\$$date\${$this->getRemoteHost()}\$\n" + "\$$id\$$this->expire\$$date\$$remoteHost\$\n" ); setcookie( self::NAME, @@ -293,8 +299,19 @@ class Auth { return true; } + /* login attempted and failed, update monitoring for rate limiting */ + $sessionCount = (int)$_SESSION['count']; + $rateLimit = (int)getenv('PREAUTH_RATE_LIMIT') ?: 4; $this->logFailedAttempt(); + + if (($sessionCount + 1) >= $rateLimit) { + /* +1 to count this failure */ + error_log("[$date] rate-limiting trigger for: $remoteHost"); + /* just reached the rate limit, block them */ + include __DIR__ . '/../400.php'; + exit(0); + } } return false; } diff --git a/template.php b/template.php index 8c5a70a..3446f14 100644 --- a/template.php +++ b/template.php @@ -1,4 +1,5 @@

getTitle(); ?>

- +
-
+
-
+