From 3f69000d275cc36de9dab53bb7a546cbe8a890e3 Mon Sep 17 00:00:00 2001 From: Carlos Arias Date: Sat, 11 Jul 2026 09:01:49 -0500 Subject: [PATCH] feat: never throttle trusted server-local callers + harden session cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two self-contained framework fixes, no schema dependencies: - throttle(): exempt trusted callers via new Functions::isTrustedIp() — loopback, the server's own IP, and an optional TRUSTED_IPS config allowlist (IPs/CIDRs). Fixes the SSG build (which fetches the read API from the box thousands of times per build) tripping the public rate limit and baking empty data into the deploy. Public client IPs stay limited. - public/index.php: set secure session cookie params (httponly, SameSite=Lax, secure on https) before session_start, so session/login state rides on a hardened cookie. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G --- api/app/Controllers/JsonController.php | 6 ++++++ api/app/Helpers/Functions.php | 25 +++++++++++++++++++++++++ api/public/index.php | 12 ++++++++++++ 3 files changed, 43 insertions(+) diff --git a/api/app/Controllers/JsonController.php b/api/app/Controllers/JsonController.php index f5b74d7..da5f80e 100644 --- a/api/app/Controllers/JsonController.php +++ b/api/app/Controllers/JsonController.php @@ -24,6 +24,12 @@ class JsonController extends \Controller protected function throttle(string $key, int $max, int $window): bool { $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + // Trusted server-local / whitelisted callers are never throttled: the static-site build + // fetches the read API from the box itself thousands of times per build. The throttle is + // for public abuse (real client IPs), not our own build. See Functions::isTrustedIp. + if (\Functions::isTrustedIp($ip)) { + return false; + } $count = (int) \Db::getValue( "SELECT COUNT(*) FROM `api_requests` WHERE `requesting_ip` = ? AND `request` = ? diff --git a/api/app/Helpers/Functions.php b/api/app/Helpers/Functions.php index daebe19..6c25f36 100644 --- a/api/app/Helpers/Functions.php +++ b/api/app/Helpers/Functions.php @@ -186,6 +186,31 @@ class Functions { + /** + * True for trusted, server-local / whitelisted callers that should bypass public rate + * limits (e.g. the SSG build hammering the read API from the box itself). + * - loopback (127.0.0.1, ::1) + * - the server's own address (same-host request) + * - anything in the optional TRUSTED_IPS config list (comma-separated IPs or CIDRs — + * e.g. the server's public IP, an internal build host, or a private range like 10.0.0.0/8) + * Deliberately does NOT blanket-trust all private ranges: behind a private-IP reverse proxy + * that would exempt every visitor. Add specific ranges to TRUSTED_IPS if your setup wants them. + */ + public static function isTrustedIp($ip) { + if ($ip === '127.0.0.1' || $ip === '::1') return true; + $serverAddr = $_SERVER['SERVER_ADDR'] ?? ''; + if ($serverAddr !== '' && $ip === $serverAddr) return true; + if (defined('TRUSTED_IPS') && TRUSTED_IPS) { + foreach (array_filter(array_map('trim', explode(',', TRUSTED_IPS))) as $entry) { + if ($ip === $entry) return true; + if (strpos($entry, '/') !== false + && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) + && self::ip_in_range($ip, $entry)) return true; + } + } + return false; + } + public static function ip_in_range($ip, $range) { if (strpos($range, '/') == false) $range .= '/32'; diff --git a/api/public/index.php b/api/public/index.php index efc71a4..38d06d7 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -1,4 +1,16 @@ 0, + 'path' => '/', + 'httponly' => true, + 'secure' => $https, + 'samesite' => 'Lax', + ]); +} @session_start(); require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/config.php';