adasd
This commit is contained in:
108
src/App/PublicFormProtection.php
Normal file
108
src/App/PublicFormProtection.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class PublicFormProtection
|
||||
{
|
||||
private RateLimiter $rateLimiter;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $projectRoot,
|
||||
private readonly array $config,
|
||||
) {
|
||||
$rateLimitPath = $this->resolveProjectPath((string) (($this->config['abuse_protection']['rate_limit_storage_path'] ?? 'data/public-form-rate-limit.json')));
|
||||
$this->rateLimiter = new RateLimiter($rateLimitPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @return array{success: bool, errors: array<int, string>}
|
||||
*/
|
||||
public function guard(string $scope, array $input): array
|
||||
{
|
||||
$errors = [];
|
||||
$honeypotField = (string) ($this->config['abuse_protection']['honeypot_field'] ?? 'company');
|
||||
|
||||
if (trim((string) ($input[$honeypotField] ?? '')) !== '') {
|
||||
$errors[] = 'Die Anfrage konnte nicht verarbeitet werden.';
|
||||
}
|
||||
|
||||
$rules = is_array($this->config['abuse_protection']['rules'] ?? null) ? $this->config['abuse_protection']['rules'] : [];
|
||||
$scopeRule = is_array($rules[$scope] ?? null) ? $rules[$scope] : [];
|
||||
$maxAttempts = (int) ($scopeRule['max_attempts'] ?? 5);
|
||||
$windowSeconds = (int) ($scopeRule['window_seconds'] ?? 900);
|
||||
|
||||
$keyParts = [
|
||||
$scope,
|
||||
$this->clientIp(),
|
||||
];
|
||||
|
||||
if ($scope === 'password_reset') {
|
||||
$keyParts[] = strtolower(trim((string) ($input['username'] ?? '')));
|
||||
}
|
||||
|
||||
$attempt = $this->rateLimiter->attempt($scope, implode('|', $keyParts), $maxAttempts, $windowSeconds);
|
||||
|
||||
if (!($attempt['allowed'] ?? false)) {
|
||||
$retryAfter = (int) ($attempt['retry_after'] ?? 0);
|
||||
$minutes = max(1, (int) ceil($retryAfter / 60));
|
||||
$errors[] = 'Zu viele Versuche. Bitte warte etwa ' . $minutes . ' Minute' . ($minutes === 1 ? '' : 'n') . ' und versuche es erneut.';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $errors === [],
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
public function honeypotField(): string
|
||||
{
|
||||
return (string) ($this->config['abuse_protection']['honeypot_field'] ?? 'company');
|
||||
}
|
||||
|
||||
private function clientIp(): string
|
||||
{
|
||||
$headers = [
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'REMOTE_ADDR',
|
||||
];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
$raw = trim((string) ($_SERVER[$header] ?? ''));
|
||||
|
||||
if ($raw === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($header === 'HTTP_X_FORWARDED_FOR') {
|
||||
$parts = array_map('trim', explode(',', $raw));
|
||||
$raw = (string) ($parts[0] ?? '');
|
||||
}
|
||||
|
||||
if ($raw !== '') {
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
private function resolveProjectPath(string $path): string
|
||||
{
|
||||
if ($path === '') {
|
||||
return $this->projectRoot . '/data/public-form-rate-limit.json';
|
||||
}
|
||||
|
||||
if (str_starts_with($path, '/')) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return $this->projectRoot . '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
55
src/App/RateLimiter.php
Normal file
55
src/App/RateLimiter.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class RateLimiter
|
||||
{
|
||||
private JsonStore $store;
|
||||
|
||||
public function __construct(string $storagePath)
|
||||
{
|
||||
$this->store = new JsonStore($storagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{allowed: bool, retry_after: int}
|
||||
*/
|
||||
public function attempt(string $scope, string $key, int $maxAttempts, int $windowSeconds): array
|
||||
{
|
||||
$maxAttempts = max(1, $maxAttempts);
|
||||
$windowSeconds = max(60, $windowSeconds);
|
||||
$now = time();
|
||||
$bucketKey = $scope . ':' . hash('sha256', $key);
|
||||
$payload = $this->store->read();
|
||||
$bucket = $payload[$bucketKey] ?? [
|
||||
'attempts' => [],
|
||||
];
|
||||
|
||||
$attempts = array_values(array_filter(
|
||||
is_array($bucket['attempts'] ?? null) ? $bucket['attempts'] : [],
|
||||
static fn (mixed $timestamp): bool => is_int($timestamp) && $timestamp > ($now - $windowSeconds)
|
||||
));
|
||||
|
||||
if (count($attempts) >= $maxAttempts) {
|
||||
$retryAfter = max(1, $windowSeconds - ($now - min($attempts)));
|
||||
$payload[$bucketKey] = ['attempts' => $attempts];
|
||||
$this->store->write($payload);
|
||||
|
||||
return [
|
||||
'allowed' => false,
|
||||
'retry_after' => $retryAfter,
|
||||
];
|
||||
}
|
||||
|
||||
$attempts[] = $now;
|
||||
$payload[$bucketKey] = ['attempts' => $attempts];
|
||||
$this->store->write($payload);
|
||||
|
||||
return [
|
||||
'allowed' => true,
|
||||
'retry_after' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -92,21 +92,21 @@ final class RegistrationService
|
||||
}
|
||||
|
||||
if (!$this->secretBox->isConfigured()) {
|
||||
$errors[] = 'Die Registrierungsverschlüsselung ist noch nicht konfiguriert.';
|
||||
$errors[] = 'Die Registrierung ist im Moment nicht verfügbar.';
|
||||
}
|
||||
|
||||
if ($note !== '' && mb_strlen($note) > 2000) {
|
||||
$errors[] = 'Die Zusatzinfo ist zu lang.';
|
||||
$errors[] = 'Die Hinweise für die Freischaltung sind zu lang.';
|
||||
}
|
||||
|
||||
$conflict = $this->store->findConflict($username, $email);
|
||||
|
||||
if ($conflict !== null) {
|
||||
$errors[] = 'Für diesen Benutzernamen oder diese E-Mail existiert bereits eine offene oder freigegebene Registrierung.';
|
||||
$errors[] = 'Für diesen Benutzernamen oder diese E-Mail-Adresse existiert bereits eine Registrierung.';
|
||||
}
|
||||
|
||||
if ($this->ldapProvisioner->usernameExists($username)) {
|
||||
$errors[] = 'Dieser Benutzername ist bereits im LDAP vergeben.';
|
||||
$errors[] = 'Dieser Benutzername ist leider bereits vergeben.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
@@ -170,7 +170,7 @@ final class RegistrationService
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'errors' => ['LDAP-Provisionierung bei Registrierung fehlgeschlagen: ' . $exception->getMessage()],
|
||||
'errors' => ['Die Registrierung konnte im Moment nicht gespeichert werden. Bitte versuche es später erneut.'],
|
||||
'request' => $this->store->create($request),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user