new csrf work flow
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
STORAGE_PATH=storage
|
||||
STORAGE_USER_LIMIT_GB=70
|
||||
USER=admin
|
||||
PASSWORD=admin
|
||||
PASSWORD=admin
|
||||
APP_KEY=9ccb8a8f9e47a93674db19e79d77f033fbde2f38ff426e88ca271fff13592e2b
|
||||
17
Makefile
17
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: build up down install logs bash migrate
|
||||
.PHONY: build up down install logs bash migrate key
|
||||
|
||||
|
||||
build:
|
||||
@@ -22,4 +22,17 @@ bash:
|
||||
docker compose exec app sh
|
||||
|
||||
migrate:
|
||||
docker compose exec app php /var/www/db/migrate.php
|
||||
docker compose exec app php /var/www/db/migrate.php
|
||||
|
||||
key:
|
||||
@echo "Generating new APP_KEY..."
|
||||
@KEY=$$(docker compose exec -T app php -r "echo bin2hex(random_bytes(32));"); \
|
||||
if grep -q '^APP_KEY=' .env; then \
|
||||
sed -i "s/^APP_KEY=.*/APP_KEY=$$KEY/" .env; \
|
||||
else \
|
||||
echo "APP_KEY=$$KEY" >> .env; \
|
||||
fi; \
|
||||
echo "APP_KEY set to $$KEY"; \
|
||||
echo "Restarting app container to apply new key..."; \
|
||||
make down
|
||||
make up
|
||||
@@ -26,13 +26,6 @@ use Nyholm\Psr7Server\ServerRequestCreator;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// PHP runtime
|
||||
// ---------------------------------------------------------------------
|
||||
error_reporting(E_ALL);
|
||||
|
||||
session_start();
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ENV
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -13,22 +13,21 @@ use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class AuthController
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private LoginService $loginService,
|
||||
private LoggerInterface $logger)
|
||||
private LoggerInterface $logger
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function loginForm(): string
|
||||
public function loginForm(ServerRequestInterface $request): string
|
||||
{
|
||||
$error = $_SESSION['login_error'] ?? null;
|
||||
unset($_SESSION['login_error']);
|
||||
$error = $request->getQueryParams()['error'] ?? null;
|
||||
|
||||
return View::display(new LoginViewModel(
|
||||
title: 'Login',
|
||||
error: $error,
|
||||
csrf: CsrfMiddleware::generateToken()
|
||||
csrf: CsrfMiddleware::generateToken($request)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -55,8 +54,6 @@ final readonly class AuthController
|
||||
);
|
||||
|
||||
if ($authToken !== null) {
|
||||
session_regenerate_id(true);
|
||||
|
||||
return new Response(
|
||||
302,
|
||||
[
|
||||
@@ -69,9 +66,10 @@ final readonly class AuthController
|
||||
);
|
||||
}
|
||||
|
||||
$_SESSION['login_error'] = 'Invalid credentials';
|
||||
|
||||
return new Response(302, ['Location' => '/login']);
|
||||
return new Response(
|
||||
302,
|
||||
['Location' => '/login?error=Invalid']
|
||||
);
|
||||
}
|
||||
|
||||
public function logout(ServerRequestInterface $request): Response
|
||||
@@ -82,8 +80,6 @@ final readonly class AuthController
|
||||
$this->loginService->logout($token);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
|
||||
return new Response(
|
||||
302,
|
||||
[
|
||||
|
||||
@@ -47,7 +47,7 @@ final readonly class DashboardController
|
||||
'percent' => $percent,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
} catch (Throwable $exception) {
|
||||
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
|
||||
return View::display(new DashboardViewModel(
|
||||
@@ -62,7 +62,7 @@ final readonly class DashboardController
|
||||
'folders' => [],
|
||||
],
|
||||
],
|
||||
csrf: CsrfMiddleware::generateToken(),
|
||||
csrf: CsrfMiddleware::generateToken($request),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ final readonly class DashboardController
|
||||
'folders' => $folders,
|
||||
],
|
||||
],
|
||||
csrf: CsrfMiddleware::generateToken(),
|
||||
csrf: CsrfMiddleware::generateToken($request),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ final readonly class StorageController
|
||||
public function showFolder(ServerRequestInterface $request, string $folder): string
|
||||
{
|
||||
$user = $request->getAttribute('user');
|
||||
$csrfToken = CsrfMiddleware::generateToken();
|
||||
$csrfToken = CsrfMiddleware::generateToken($request);
|
||||
|
||||
$folderPath = $this->storageService->getDriver()->getUserFolderPath($user->id, $folder);
|
||||
|
||||
@@ -327,7 +327,7 @@ final readonly class StorageController
|
||||
if (!is_array($data)) {
|
||||
return new Response(400);
|
||||
}
|
||||
|
||||
|
||||
$folder = $data['folder'] ?? '';
|
||||
$raw = $data['file_names'] ?? '[]';
|
||||
|
||||
|
||||
@@ -3,19 +3,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Middlewares;
|
||||
|
||||
use JsonException;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Random\RandomException;
|
||||
use RuntimeException;
|
||||
|
||||
final class CsrfMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private const array UNSAFE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
private const int TTL = 3600;
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
public function process(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
$path = $request->getUri()->getPath();
|
||||
@@ -25,11 +29,15 @@ final class CsrfMiddleware implements MiddlewareInterface
|
||||
}
|
||||
|
||||
if (in_array($method, self::UNSAFE_METHODS, true)) {
|
||||
$token = $request->getParsedBody()['_csrf'] ?? null;
|
||||
|
||||
$token = $_POST['_csrf'] ?? '';
|
||||
if (!isset($_SESSION['_csrf']) || $token !== $_SESSION['_csrf']) {
|
||||
return new Psr17Factory()->createResponse(403)
|
||||
->withBody(new Psr17Factory()->createStream('CSRF validation failed'));
|
||||
if (!$token || !self::validateToken($token, $request)) {
|
||||
return new Psr17Factory()
|
||||
->createResponse(403)
|
||||
->withBody(
|
||||
new Psr17Factory()
|
||||
->createStream('CSRF validation failed')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,36 +46,63 @@ final class CsrfMiddleware implements MiddlewareInterface
|
||||
|
||||
private function isExcludedPath(string $path): bool
|
||||
{
|
||||
if (str_starts_with($path, '/storage/tus')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_starts_with($path, '/api/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_starts_with($path, '/webhook/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return
|
||||
str_starts_with($path, '/storage/tus') ||
|
||||
str_starts_with($path, '/api/') ||
|
||||
str_starts_with($path, '/webhook/');
|
||||
}
|
||||
|
||||
public static function generateToken(): string
|
||||
public static function generateToken(ServerRequestInterface $request): string
|
||||
{
|
||||
if (empty($_SESSION['_csrf']) || $_SESSION['_csrf_expire'] < time()) {
|
||||
try {
|
||||
$_SESSION['_csrf'] = bin2hex(random_bytes(32));
|
||||
$_SESSION['_csrf_expire'] = time() + 3600;
|
||||
} catch (RandomException $e) {
|
||||
throw new RuntimeException(
|
||||
'Failed to generate CSRF token: ' . $e->getMessage(),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
try {
|
||||
$payload = json_encode([
|
||||
'ts' => time(),
|
||||
'ua' => hash('sha256', $request->getHeaderLine('User-Agent')),
|
||||
], JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
error_log($e->getMessage());
|
||||
}
|
||||
|
||||
return $_SESSION['_csrf'];
|
||||
$payloadB64 = base64_encode($payload);
|
||||
$signature = hash_hmac('sha256', $payloadB64, self::key(), true);
|
||||
|
||||
return $payloadB64 . '.' . base64_encode($signature);
|
||||
|
||||
}
|
||||
|
||||
public static function validateToken(
|
||||
string $token,
|
||||
ServerRequestInterface $request
|
||||
): bool
|
||||
{
|
||||
[$payloadB64, $sigB64] = explode('.', $token, 2) + [null, null];
|
||||
|
||||
if (!$payloadB64 || !$sigB64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = hash_hmac('sha256', $payloadB64, self::key(), true);
|
||||
if (!hash_equals($expected, base64_decode($sigB64))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_decode(base64_decode($payloadB64), true);
|
||||
if (!isset($payload['ts'], $payload['ua'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (time() - $payload['ts'] > self::TTL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uaHash = hash('sha256', $request->getHeaderLine('User-Agent'));
|
||||
|
||||
return hash_equals($payload['ua'], $uaHash);
|
||||
}
|
||||
|
||||
private static function key(): string
|
||||
{
|
||||
return $_ENV['APP_KEY']
|
||||
?? throw new RuntimeException('APP_KEY not defined');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user