From 0c60f3d15ee0247cea19fee1fba9d8c4cdd89355 Mon Sep 17 00:00:00 2001 From: JensFalk Date: Tue, 22 Sep 2026 10:47:30 +0200 Subject: [PATCH] AltchaService: Angriffsprotokoll (Datei-Log + Zugriffsschluessel) hinzugefuegt --- .../src/Service/AltchaService.php | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/fp_altcha_spamschutz/src/Service/AltchaService.php b/fp_altcha_spamschutz/src/Service/AltchaService.php index 0cc4c82..5be7a1d 100644 --- a/fp_altcha_spamschutz/src/Service/AltchaService.php +++ b/fp_altcha_spamschutz/src/Service/AltchaService.php @@ -102,6 +102,120 @@ class AltchaService return (int) $this->getConfigValue('fp_altcha_honeypot_threshold', '50'); } + /** + * Das Angriffsprotokoll ist unabhaengig vom (nur fuer Tests gedachten) Debug-Logging und + * standardmaessig aktiv: es haelt fortlaufend die zuletzt abgelehnten Anfragen fest. + */ + public function isAttackLogEnabled(): bool + { + return $this->getConfigValue('fp_altcha_attack_log', 'on') === 'on'; + } + + /** + * Haengt einen abgelehnten Versuch an die rotierende Protokolldatei an (siehe log.php fuer + * die Ansicht). Bewusst komplett fehlertolerant: schlaegt das Schreiben fehl (z. B. keine + * Schreibrechte), wird das still ignoriert -- die eigentliche Formularpruefung darf davon + * nie beeintraechtigt werden. + * + * @param array $entry + */ + public function appendAttackLogEntry(array $entry): void + { + try { + $dir = $this->pluginDir . '/data'; + $file = $dir . '/fp_altcha_attacklog.jsonl'; + + if (!\is_dir($dir)) { + @\mkdir($dir, 0755, true); + } + + $line = \json_encode($entry, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + if (!\is_string($line)) { + return; + } + + @\file_put_contents($file, $line . "\n", \FILE_APPEND | \LOCK_EX); + + $this->trimAttackLog($file); + } catch (\Throwable $e) { + // Protokollieren ist ein Zusatzfeature -- ein Fehler hier darf niemals die eigentliche + // Formularpruefung stoeren. + } + } + + /** + * Kuerzt die Protokolldatei auf die konfigurierte Hoechstanzahl an Eintraegen. Laeuft nur + * mit geringer Wahrscheinlichkeit bei jedem Aufruf (statt immer), um bei vielen gleichzeitigen + * Anfragen nicht staendig die komplette Datei einlesen zu muessen. + */ + private function trimAttackLog(string $file): void + { + if (\random_int(1, 20) !== 1) { + return; + } + + if (!\is_file($file)) { + return; + } + + $lines = @\file($file, \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES); + if ($lines === false) { + return; + } + + $max = $this->getAttackLogMaxEntries(); + if (\count($lines) <= $max) { + return; + } + + $trimmed = \array_slice($lines, -$max); + @\file_put_contents($file, \implode("\n", $trimmed) . "\n", \LOCK_EX); + } + + private function getAttackLogMaxEntries(): int + { + $value = (int) $this->getConfigValue('fp_altcha_attack_log_max', '300'); + if ($value < 20 || $value > 5000) { + return 300; + } + + return $value; + } + + /** + * Liefert den Zugriffsschluessel fuer die eigenstaendige Protokoll-Ansicht (log.php). Wird + * beim allerersten Bedarf automatisch zufaellig erzeugt und in einer eigenen Datei im + * Plugin-Verzeichnis gespeichert -- exakt nach demselben Muster wie der HMAC-Geheimschluessel + * (siehe getOrCreateAutoSecret()). + */ + public function getLogViewToken(): string + { + $dir = $this->pluginDir . '/data'; + $file = $dir . '/fp_altcha_log_token.php'; + + if (\is_file($file)) { + $value = include $file; + if (\is_string($value) && $value !== '') { + return $value; + } + } + + $token = \bin2hex(\random_bytes(24)); + + if (!\is_dir($dir)) { + @\mkdir($dir, 0755, true); + } + + @\file_put_contents( + $file, + "