Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c1128ca4d | ||
|
|
17f5c7b98f | ||
|
|
2dbf11753a | ||
|
|
1e6fb0be5b | ||
|
|
bc27582a0d |
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 JensFalk
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,32 +1,5 @@
|
||||
# falk_plus ALTCHA Spam- und Botschutz
|
||||
|
||||
Selbst gehostetes, quelloffenes Spam- und Botschutz-Plugin (ALTCHA Proof-of-Work, MIT-Lizenz) für JTL-Shop 5 -- schützt Registrierung, Newsletter-Anmeldung und Kontaktformular, ohne Daten an Dritte weiterzugeben (kein Google reCAPTCHA, keine Umleitung über Cloudflare oder ähnliche Dienste).
|
||||
|
||||
## Enthält
|
||||
|
||||
- `fp_altcha_spamschutz/` -- komplettes, installierbares JTL-Shop-5-Plugin (Plugin-ID `fp_altcha_spamschutz`)
|
||||
- `Bootstrap.php` -- Hook-Registrierung (EventDispatcher)
|
||||
- `info.xml` -- Plugin-Manifest / Einstellungen
|
||||
- `src/Service/AltchaService.php` -- Challenge-Erzeugung & Verifikation (ALTCHA V1)
|
||||
- `src/Handler/TemplateHandler.php` -- Widget-Einbindung ins Formular-HTML
|
||||
- `src/Handler/ValidationHandler.php` -- Prüfung bei Registrierung, Newsletter-Anmeldung und Kontaktformular
|
||||
- `assets/fp-altcha.js` -- client-seitiger Proof-of-Work-Löser
|
||||
|
||||
Die installierbare Plugin-ZIP enthält zusätzlich die unverändert vendorierte ALTCHA-PHP-Bibliothek ([altcha-org/altcha-lib-php](https://github.com/altcha-org/altcha-lib-php), MIT-Lizenz) unter `src/Vendor/AltchaOrg/Altcha/V1/`. Um Drittanbieter-Code hier nicht zu duplizieren, ist sie in diesem Repo nicht enthalten.
|
||||
|
||||
## Hintergrund
|
||||
|
||||
Anlass war ein Bot-Problem bei einem Kunden: über Wochen liefen massenhaft Fake-Registrierungen und Newsletter-Anmeldungen mit immer demselben Muster auf ("Test"/"Test User" in Vorname/Nachname/Ort/Straße, aber echte, unterschiedliche Fremd-E-Mail-Adressen). Rate-Limiting und andere getestete Maßnahmen konnten das Problem nicht zuverlässig lösen, da deren Prüfungen an anderer Stelle ansetzen (z. B. an Kommentar-/Nachrichtenfeldern, die weder das Registrierungs- noch das Newsletter-Formular besitzen). Dieses Plugin prüft stattdessen direkt beim Absenden von Registrierung, Newsletter-Anmeldung und Kontaktformular selbst -- unabhängig davon, welche Felder das Formular sonst enthält.
|
||||
|
||||
## Installation / Download
|
||||
|
||||
Die direkt installierbare Plugin-ZIP (inkl. der mitgelieferten ALTCHA-PHP-Bibliothek) gibt es aktuell auf diesen Wegen:
|
||||
|
||||
1. **Fertige ZIP unter [Releases](https://vw.falk.plus/JensFalk/JTL-Shop-ALTCHA-Spamschutz/releases)**: zu jeder Version gibt es dort einen Eintrag; sobald eine ZIP-Datei als Anhang vorliegt, lässt sie sich ohne weitere Anpassung direkt im JTL-Shop-Backend unter *Plugins → Plugin hochladen* installieren.
|
||||
2. **Quellcode-Download über "Code → Download ZIP"**: dieser Weg liefert den reinen Quellcode dieses Repos (ohne die ALTCHA-PHP-Bibliothek) und packt ihn zusätzlich in einen Ordner mit Repo- und Branch-Namen. Für die Installation muss danach der enthaltene Ordner `fp_altcha_spamschutz` neu, direkt als Wurzelverzeichnis, gezippt werden.
|
||||
|
||||
Ausführliche Installations-, Test- und Hintergrundinfos im Guide: [jtl-shop:altcha-spamschutz:start](https://guide.falk.plus/doku.php?id=jtl-shop:altcha-spamschutz:start)
|
||||
|
||||
## Feedback
|
||||
weiteres siehe im [Guide](https://guide.falk.plus/doku.php?id=jtl-shop:altcha-spamschutz:start)
|
||||
|
||||
Fragen, Bugs oder Verbesserungsvorschläge bitte als [Issue in diesem Repo](https://vw.falk.plus/JensFalk/JTL-Shop-ALTCHA-Spamschutz/issues) melden.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<XMLVersion>101</XMLVersion>
|
||||
<MinShopVersion>5.2.0</MinShopVersion>
|
||||
<CreateDate>2026-09-21</CreateDate>
|
||||
<Version>1.2.1</Version>
|
||||
<Version>1.3.0</Version>
|
||||
<Install>
|
||||
<FlushTags>CACHING_GROUP_TEMPLATE</FlushTags>
|
||||
<!-- Hooks werden nicht über XML, sondern modern per EventDispatcher in Bootstrap.php registriert. -->
|
||||
@@ -68,6 +68,33 @@
|
||||
<ValueName>fp_altcha_expiry_seconds</ValueName>
|
||||
</Setting>
|
||||
</Settingslink>
|
||||
<Settingslink sort="5">
|
||||
<Name>Namensmuster-Filter</Name>
|
||||
<Setting type="text" initialValue="Test,Test User" sort="0" conf="Y">
|
||||
<Name>Gesperrte Namensmuster</Name>
|
||||
<Description>Kommagetrennte Liste von Begriffen. Eine Anfrage wird zusätzlich abgelehnt, wenn Vorname, Nachname, Ort oder Straße exakt (ohne Beachtung von Groß-/Kleinschreibung) einem dieser Begriffe entspricht -- unabhängig vom Ergebnis der Sicherheitsprüfung. Damit lassen sich auch Bots abfangen, die die Sicherheitsprüfung technisch lösen können. Feld leeren, um diese zusätzliche Prüfung zu deaktivieren. Bei einem neuen/anderen erkannten Muster hier einfach ergänzen, z. B. Test,Test User,Testkunde.</Description>
|
||||
<ValueName>fp_altcha_name_blocklist</ValueName>
|
||||
</Setting>
|
||||
</Settingslink>
|
||||
<Settingslink sort="8">
|
||||
<Name>Project Honey Pot</Name>
|
||||
<Setting type="text" initialValue="" sort="0" conf="Y">
|
||||
<Name>http:BL Zugangsschlüssel</Name>
|
||||
<Description>Eigener Zugangsschlüssel von projecthoneypot.org für die http:BL-Reputationsabfrage der Besucher-IP. Leer lassen, um diese zusätzliche Prüfung zu deaktivieren. Ein kostenloser Schlüssel lässt sich unter projecthoneypot.org anlegen.</Description>
|
||||
<ValueName>fp_altcha_honeypot_key</ValueName>
|
||||
</Setting>
|
||||
<Setting type="selectbox" initialValue="50" sort="10" conf="Y">
|
||||
<Name>Mindest-Bedrohungsstufe für Ablehnung</Name>
|
||||
<Description>Ab welcher von http:BL gemeldeten Bedrohungsstufe (0-255) eine Anfrage zusätzlich abgelehnt wird. Niedrigerer Wert = strenger.</Description>
|
||||
<ValueName>fp_altcha_honeypot_threshold</ValueName>
|
||||
<SelectboxOptions>
|
||||
<Option value="0" sort="1"><![CDATA[Sehr streng (0)]]></Option>
|
||||
<Option value="25" sort="2"><![CDATA[Streng (25)]]></Option>
|
||||
<Option value="50" sort="3"><![CDATA[Mittel (50, empfohlen)]]></Option>
|
||||
<Option value="75" sort="4"><![CDATA[Locker (75)]]></Option>
|
||||
</SelectboxOptions>
|
||||
</Setting>
|
||||
</Settingslink>
|
||||
<Settingslink sort="10">
|
||||
<Name>Formulare</Name>
|
||||
<Setting type="checkbox" initialValue="on" sort="0" conf="Y">
|
||||
|
||||
@@ -27,6 +27,11 @@ use Plugin\fp_altcha_spamschutz\src\Service\AltchaService;
|
||||
* Verhalten zu verlassen, wird hier bewusst derselbe frühe, hook-unabhängige Ansatz wie bei der
|
||||
* Newsletter-Anmeldung verwendet: direkte Prüfung von $_POST, noch bevor JTL-Shop das Formular
|
||||
* verarbeitet (siehe guardContactSubmission()).
|
||||
*
|
||||
* Zusätzlich zur ALTCHA-Prüfung gibt es zwei weitere, unabhängige Ablehnungsgründe (siehe
|
||||
* matchesBlockedPattern() und isFlaggedByHoneypot()): beide fangen gezielt Bots ab, die zwar
|
||||
* JavaScript ausführen und die Rechenaufgabe lösen können, sich aber an einem bekannten
|
||||
* Namensmuster oder einer bereits einschlägig aufgefallenen IP-Adresse erkennen lassen.
|
||||
*/
|
||||
class ValidationHandler
|
||||
{
|
||||
@@ -47,10 +52,16 @@ class ValidationHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->altchaService->verifyPost()) {
|
||||
$blockedByAltcha = !$this->altchaService->verifyPost();
|
||||
$blockedByPattern = $this->matchesBlockedPattern();
|
||||
$blockedByHoneypot = $this->isFlaggedByHoneypot();
|
||||
|
||||
if (!$blockedByAltcha && !$blockedByPattern && !$blockedByHoneypot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logRejection('Registrierung', $blockedByAltcha, $blockedByPattern, $blockedByHoneypot);
|
||||
|
||||
if (isset($args['fehlendeAngaben']) && \is_array($args['fehlendeAngaben'])) {
|
||||
$args['fehlendeAngaben'][] = 'Sicherheitsprüfung';
|
||||
}
|
||||
@@ -87,10 +98,16 @@ class ValidationHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->altchaService->verifyPost()) {
|
||||
$blockedByAltcha = !$this->altchaService->verifyPost();
|
||||
$blockedByPattern = $this->matchesBlockedPattern();
|
||||
$blockedByHoneypot = $this->isFlaggedByHoneypot();
|
||||
|
||||
if (!$blockedByAltcha && !$blockedByPattern && !$blockedByHoneypot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logRejection('Newsletter-Anmeldung', $blockedByAltcha, $blockedByPattern, $blockedByHoneypot);
|
||||
|
||||
// Feld entfernen, damit JTL-Shop die Anmeldung nicht als "abonnieren"-Request erkennt
|
||||
// und stattdessen nur das (leere) Formular erneut anzeigt.
|
||||
unset($_POST['abonnieren']);
|
||||
@@ -123,10 +140,16 @@ class ValidationHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->altchaService->verifyPost()) {
|
||||
$blockedByAltcha = !$this->altchaService->verifyPost();
|
||||
$blockedByPattern = $this->matchesBlockedPattern();
|
||||
$blockedByHoneypot = $this->isFlaggedByHoneypot();
|
||||
|
||||
if (!$blockedByAltcha && !$blockedByPattern && !$blockedByHoneypot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logRejection('Kontaktformular', $blockedByAltcha, $blockedByPattern, $blockedByHoneypot);
|
||||
|
||||
// Pflichtfeld leeren, damit JTL-Shops eigene Prüfung ("Pflichtfeld nicht ausgefüllt")
|
||||
// die Anfrage abweist -- der Besucher bekommt die normale, vertraute Fehlermeldung.
|
||||
unset($_POST['nachricht']);
|
||||
@@ -165,4 +188,103 @@ class ValidationHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft Vorname, Nachname, Ort und Straße aus dem aktuellen POST-Request gegen die in den
|
||||
* Plugin-Einstellungen hinterlegte Liste bekannter Bot-Namensmuster (z. B. "Test", "Test
|
||||
* User"). Verglichen wird der komplette, getrimmte Feldwert ohne Beachtung von Groß-/
|
||||
* Kleinschreibung -- absichtlich kein Teilstring-Vergleich, damit z. B. eine echte Straße
|
||||
* oder ein echter Ort, der zufällig den Begriff enthält, nicht fälschlich blockiert wird.
|
||||
*/
|
||||
private function matchesBlockedPattern(): bool
|
||||
{
|
||||
$patterns = $this->altchaService->getNameBlocklist();
|
||||
if ($patterns === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['vorname', 'nachname', 'ort', 'strasse'] as $field) {
|
||||
$value = isset($_POST[$field]) && \is_string($_POST[$field]) ? \trim($_POST[$field]) : '';
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (\mb_strtolower($value) === \mb_strtolower($pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt Project Honey Pot (http:BL) fuer die IP-Adresse des aktuellen Besuchers ab, sofern
|
||||
* in den Einstellungen ein Zugangsschluessel hinterlegt ist. http:BL unterstuetzt nur IPv4;
|
||||
* bei IPv6-Adressen oder wenn die Abfrage fehlschlaegt, wird sicherheitshalber NICHT
|
||||
* blockiert (lieber ein Bot mehr durchlassen als echte Kunden faelschlich abweisen).
|
||||
*
|
||||
* Wichtig: es wird bewusst nur $_SERVER['REMOTE_ADDR'] verwendet, nicht z. B.
|
||||
* X-Forwarded-For -- dieser Header liesse sich von einem Bot beliebig faelschen und wuerde
|
||||
* die Pruefung wirkungslos machen. Laeuft der Shop hinter einem Reverse-Proxy, der die echte
|
||||
* Besucher-IP nicht in REMOTE_ADDR durchreicht, liefert diese Pruefung keine sinnvollen
|
||||
* Ergebnisse mehr.
|
||||
*/
|
||||
private function isFlaggedByHoneypot(): bool
|
||||
{
|
||||
if (!$this->altchaService->isHoneypotEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
if (!\is_string($ip) || \filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reversedIp = \implode('.', \array_reverse(\explode('.', $ip)));
|
||||
$accessKey = $this->altchaService->getHoneypotApiKey();
|
||||
$query = $accessKey . '.' . $reversedIp . '.dnsbl.httpbl.org';
|
||||
|
||||
$records = @\dns_get_record($query, \DNS_A);
|
||||
if (!\is_array($records) || $records === [] || !isset($records[0]['ip'])) {
|
||||
// Keine Antwort/kein Eintrag -- IP ist bei http:BL nicht (mehr) bekannt.
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = \explode('.', (string) $records[0]['ip']);
|
||||
if (\count($parts) !== 4 || $parts[0] !== '127') {
|
||||
// Unerwartetes Antwortformat -- sicherheitshalber nicht blocken.
|
||||
return false;
|
||||
}
|
||||
|
||||
$threatScore = (int) $parts[2];
|
||||
$type = (int) $parts[3];
|
||||
|
||||
if ($type === 0) {
|
||||
// Typ 0 = ausschliesslich als Suchmaschine gelistet, kein Ablehnungsgrund.
|
||||
return false;
|
||||
}
|
||||
|
||||
return $threatScore >= $this->altchaService->getHoneypotThreshold();
|
||||
}
|
||||
|
||||
private function logRejection(
|
||||
string $formular,
|
||||
bool $blockedByAltcha,
|
||||
bool $blockedByPattern,
|
||||
bool $blockedByHoneypot
|
||||
): void {
|
||||
if (!$this->altchaService->isDebug()) {
|
||||
return;
|
||||
}
|
||||
|
||||
error_log(\sprintf(
|
||||
'[fp_altcha_spamschutz] %s abgelehnt (altcha=%s, namensmuster=%s, honeypot=%s)',
|
||||
$formular,
|
||||
$blockedByAltcha ? 'ja' : 'nein',
|
||||
$blockedByPattern ? 'ja' : 'nein',
|
||||
$blockedByHoneypot ? 'ja' : 'nein'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,45 @@ class AltchaService
|
||||
return $this->getConfigValue('fp_altcha_debug', '') === 'on';
|
||||
}
|
||||
|
||||
/**
|
||||
* Kommagetrennte Liste bekannter Bot-Namensmuster (z. B. "Test,Test User"), konfigurierbar
|
||||
* in den Plugin-Einstellungen. Leere/nur aus Leerzeichen bestehende Einträge werden ignoriert.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNameBlocklist(): array
|
||||
{
|
||||
$raw = $this->getConfigValue('fp_altcha_name_blocklist', '');
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = \array_map('trim', \explode(',', $raw));
|
||||
|
||||
return \array_values(\array_filter($parts, static fn(string $p): bool => $p !== ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Project Honey Pot (http:BL) ist nur aktiv, wenn ein Zugangsschluessel hinterlegt wurde.
|
||||
*/
|
||||
public function isHoneypotEnabled(): bool
|
||||
{
|
||||
return $this->getHoneypotApiKey() !== '';
|
||||
}
|
||||
|
||||
public function getHoneypotApiKey(): string
|
||||
{
|
||||
return $this->getConfigValue('fp_altcha_honeypot_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ab dieser von http:BL gemeldeten Bedrohungsstufe (0-255) wird zusaetzlich abgelehnt.
|
||||
*/
|
||||
public function getHoneypotThreshold(): int
|
||||
{
|
||||
return (int) $this->getConfigValue('fp_altcha_honeypot_threshold', '50');
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine neue Prüfung und liefert sie als Array, das 1:1 als JSON in die Seite
|
||||
* eingebettet werden kann (siehe TemplateHandler).
|
||||
|
||||
Reference in New Issue
Block a user