diff --git a/php/public/clean-history.js b/php/public/clean-history.js index 17bd4ac4..26b8f642 100644 --- a/php/public/clean-history.js +++ b/php/public/clean-history.js @@ -6,6 +6,10 @@ // The target URL is passed via the script tag's data-target attribute. // document.currentScript is only available during synchronous script execution // (not with defer/async), so this script is loaded without those attributes. +// +// We replace with location.pathname only (no query string, no hash), which +// intentionally strips the ?token=… parameter and any hash fragment from the +// recorded history entry. const target = document.currentScript.dataset.target; history.replaceState(null, '', location.pathname); window.location.replace(target); diff --git a/php/src/Controller/DockerController.php b/php/src/Controller/DockerController.php index 3c0ff8f0..8fe99ee8 100644 --- a/php/src/Controller/DockerController.php +++ b/php/src/Controller/DockerController.php @@ -167,6 +167,8 @@ readonly class DockerController { // The password has been passed to the borgbackup container's environment. // Clear it from the persistent config so it is not kept at rest longer than needed. + // ConfigurationManager.set() overwrites the stored value with an empty string and + // immediately writes the updated config to disk, so the plaintext password is removed. $this->configurationManager->borgRestorePassword = ''; // End streaming response diff --git a/php/src/Controller/LoginController.php b/php/src/Controller/LoginController.php index 00a1a2fc..41b86ae7 100644 --- a/php/src/Controller/LoginController.php +++ b/php/src/Controller/LoginController.php @@ -40,12 +40,18 @@ readonly class LoginController { $response->getBody()->write("Unable to determine client IP. Login refused."); return $response->withStatus(403); } - $rateLimitKey = 'login_attempts_' . hash('sha256', $ip); + + // Use HMAC to avoid leaking which IPs are being tracked via predictable cache-key names. + $hmacKey = (string)(apcu_fetch('login_rate_limit_hmac_key') ?: ''); + if ($hmacKey === '') { + $hmacKey = bin2hex(random_bytes(16)); + apcu_add('login_rate_limit_hmac_key', $hmacKey); + } + $rateLimitKey = 'login_attempts_' . hash_hmac('sha256', $ip, $hmacKey); $attempts = (int)(apcu_fetch($rateLimitKey) ?: 0); if ($attempts >= self::MAX_FAILED_ATTEMPTS) { - // Keep a delay even when blocked so the 429 itself isn't a timing oracle. - sleep(5); + // Return 429 immediately; the rate limit itself is sufficient protection. $response->getBody()->write("Too many failed login attempts. Please try again later."); return $response->withStatus(429); }