mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-05-21 10:50:10 +00:00
security: fix brute-force protection, token history leak, streaming XSS, borg password persistence, and missing cache headers
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/f1016d36-0771-46e0-992c-95ce22594414 Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
99ea91c5ef
commit
3e72f06d32
6
php/public/clean-history.js
Normal file
6
php/public/clean-history.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// This script is loaded after a successful token-based login.
|
||||
// It replaces the browser's current history entry (stripping the token from the
|
||||
// URL) before navigating to the main AIO page, so the token is never left in
|
||||
// the browser history and cannot be accidentally exposed via the back-button.
|
||||
history.replaceState(null, '', location.pathname);
|
||||
window.location.replace('../../');
|
||||
@@ -181,7 +181,7 @@ $app->get('/containers', function (Request $request, Response $response, array $
|
||||
'community_containers' => $configurationManager->listAvailableCommunityContainers(),
|
||||
'community_containers_enabled' => $configurationManager->aioCommunityContainers,
|
||||
'bypass_container_update' => $bypass_container_update,
|
||||
]);
|
||||
])->withHeader('Cache-Control', 'no-store');
|
||||
})->setName('profile');
|
||||
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
|
||||
$view = Twig::fromRequest($request);
|
||||
@@ -209,7 +209,7 @@ $app->get('/setup', function (Request $request, Response $response, array $args)
|
||||
[
|
||||
'password' => $setup->Setup(),
|
||||
]
|
||||
);
|
||||
)->withHeader('Cache-Control', 'no-store');
|
||||
});
|
||||
$app->get('/log', function (Request $request, Response $response, array $args) use ($container) {
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
@@ -165,6 +165,10 @@ readonly class DockerController {
|
||||
$id = 'nextcloud-aio-borgbackup';
|
||||
$this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody);
|
||||
|
||||
// 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.
|
||||
$this->configurationManager->borgRestorePassword = '';
|
||||
|
||||
// End streaming response
|
||||
$this->finalizeStreamingResponse($nonbufResp);
|
||||
return $nonbufResp;
|
||||
@@ -339,7 +343,7 @@ readonly class DockerController {
|
||||
|
||||
$body = $nonbufResp->getBody();
|
||||
$addToStreamingResponseBody = function (string $message) use ($body) : void {
|
||||
$body->write("<div>$message</div>");
|
||||
$body->write('<div>' . htmlspecialchars($message, ENT_QUOTES | ENT_HTML5) . '</div>');
|
||||
};
|
||||
|
||||
$this->dockerActionManager->SystemPrune($addToStreamingResponseBody);
|
||||
@@ -430,7 +434,7 @@ readonly class DockerController {
|
||||
// if it'll actually pull an image), but which should not need to know anything about the
|
||||
// wanted markup or formatting.
|
||||
$addToStreamingResponseBody = function (Container $container, string $message) use ($nonbufResp) : void {
|
||||
$nonbufResp->getBody()->write("<div>{$container->displayName}: {$message}</div>");
|
||||
$nonbufResp->getBody()->write('<div>' . htmlspecialchars($container->displayName, ENT_QUOTES | ENT_HTML5) . ': ' . htmlspecialchars($message, ENT_QUOTES | ENT_HTML5) . '</div>');
|
||||
};
|
||||
|
||||
return $addToStreamingResponseBody;
|
||||
|
||||
@@ -17,17 +17,45 @@ readonly class LoginController {
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of failed login attempts allowed within the rate-limit window.
|
||||
*/
|
||||
private const int MAX_FAILED_ATTEMPTS = 10;
|
||||
|
||||
/**
|
||||
* Duration in seconds during which failed attempts are counted (and for which a lockout lasts).
|
||||
*/
|
||||
private const int RATE_LIMIT_WINDOW_SEC = 300;
|
||||
|
||||
public function TryLogin(Request $request, Response $response, array $args) : Response {
|
||||
if (!$this->dockerActionManager->isLoginAllowed()) {
|
||||
$response->getBody()->write("The login is blocked since Nextcloud is running.");
|
||||
return $response->withHeader('Location', '.')->withStatus(422);
|
||||
}
|
||||
|
||||
// Per-IP rate limiting: block after MAX_FAILED_ATTEMPTS failures within RATE_LIMIT_WINDOW_SEC.
|
||||
$ip = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
|
||||
$rateLimitKey = 'login_attempts_' . hash('sha256', $ip);
|
||||
$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);
|
||||
$response->getBody()->write("Too many failed login attempts. Please try again later.");
|
||||
return $response->withHeader('Location', '.')->withStatus(429);
|
||||
}
|
||||
|
||||
$password = $request->getParsedBody()['password'] ?? '';
|
||||
if($this->authManager->CheckCredentials($password)) {
|
||||
// Clear the counter on success.
|
||||
apcu_delete($rateLimitKey);
|
||||
$this->authManager->SetAuthState(true);
|
||||
return $response->withHeader('Location', '.')->withStatus(201);
|
||||
}
|
||||
|
||||
// Increment the failed-attempts counter (expires after RATE_LIMIT_WINDOW_SEC seconds).
|
||||
apcu_store($rateLimitKey, $attempts + 1, self::RATE_LIMIT_WINDOW_SEC);
|
||||
|
||||
// Punish failed auth attempts with a delay, as a very simple means against bots.
|
||||
sleep(5);
|
||||
|
||||
@@ -39,7 +67,17 @@ readonly class LoginController {
|
||||
$token = $request->getQueryParams()['token'] ?? '';
|
||||
if($this->authManager->CheckToken($token)) {
|
||||
$this->authManager->SetAuthState(true);
|
||||
return $response->withHeader('Location', '../..')->withStatus(302);
|
||||
// Return a minimal HTML page that uses JavaScript to replace the browser's
|
||||
// current history entry (removing the token from it) before navigating to
|
||||
// the main AIO page. This prevents the token from remaining in browser history.
|
||||
$response->getBody()->write(
|
||||
'<!DOCTYPE html>' .
|
||||
'<html lang="en">' .
|
||||
'<head><script src="../../clean-history.js"></script></head>' .
|
||||
'<body></body>' .
|
||||
'</html>'
|
||||
);
|
||||
return $response->withHeader('Content-Type', 'text/html; charset=utf-8')->withStatus(200);
|
||||
}
|
||||
|
||||
// Punish failed auth attempts with a delay, as a very simple means against bots.
|
||||
|
||||
Reference in New Issue
Block a user