ALTCHA: Technische Details
ALTCHA Spam-Schutz für Sylius - Ablauf der Prüfkette als Diagramm, die drei Prüfwege (Formular, Route, Event), Plugin-Struktur und die sechs Entities mit eigenen Tabellen
ALTCHA: Technische Details
Live Demo: Teste dieses und weitere Plugins live — ohne Installation, ohne Risiko: demo.markus-michalski.net
Ablauf der Prüfungen
graph TD
A["Absendung / Request"] --> B{"Channel aktiviert?"}
B -->|"Nein"| C["Ungeschützt"]
B -->|"Ja"| D{"Schutzziel angehakt?"}
D -->|"Nein"| C
D -->|"Ja"| E["Rate-Limit"]
E -->|"überschritten"| F["429 / rate_limited"]
E -->|"ok"| G["Honeypot + Ausfüllzeit"]
G -->|"fehlgeschlagen"| H["Abgelehnt - Formularfehler / 422"]
G -->|"ok"| I{"Wegwerf-Mail / Gibberish aktiv?"}
I -->|"auffällig"| H
I -->|"aus oder bestanden"| J["ALTCHA-Lösung prüfen"]
J -->|"ungültig oder bereits verwendet"| H
J -->|"gültig"| K["Absendung akzeptiert"]
Rate-Limit, Honeypot, Mindest-Ausfüllzeit und die Inhaltsprüfungen laufen vor der Proof-of-Work-Prüfung (die günstigsten zuerst), und zwar bei allen drei Prüfwegen gleich: Ein Schutzziel wird immer auf dieselbe Weise geprüft, egal über welchen Weg es erreicht wird.
Die drei Prüfwege
graph LR
A["Twig-Formular (Storefront)"] --> D["Formularvalidierung"]
B["Route (Login / Checkout / Shop API)"] --> E["Routenprüfung bei kernel.request"]
C["Plugin-Event (Support-Formular, Newsletter)"] --> F["Event-Listener"]
D --> G["Prüfkette"]
E --> G
F --> G
G --> H["Channel-Einstellungen"]
| Prüfweg | Verwendet für | Läuft |
|---|---|---|
| Formularvalidierung | Registrierung, Kontakt, "Passwort vergessen", eigene Formulare | Nach den regulären Constraints, ein aus anderen Gründen ungültiges Formular verbraucht also keine Challenge. Neuberechnungen einer Live-Component werden übersprungen. |
| Routenprüfung | Login, Login im Checkout, Operationen der Shop API | Bei kernel.request, vor der Firewall. Bei Storefront-Routen ist die Locale dann bereits gesetzt; bei API-Routen setzt Sylius sie erst später aus dem Channel, deshalb liest die Prüfung die Locale dort aus dem Locale-Kontext von Sylius. |
| Event-Listener | Support-Formular, Newsletter und jedes Schutzziel, das Events angibt | Auf dem eigenen ablehnbaren Event des anderen Plugins (Storefront und API gleichermaßen) |
Eine Ablehnung bei der Formularvalidierung wird als Formularfehler hinzugefügt und zusätzlich als Flash-Meldung angezeigt, weil die Sylius-Shop-Templates Felder einzeln rendern und nie die Fehler des Formulars selbst.
Plugin-Struktur
src/
├── Challenge/ # Challenge-Erzeugung und Signatur
├── Command/ # SetupCommand, PurgeCommand, UpdateDisposableDomainsCommand
├── Controller/ # ChallengeController, BlockedDomainsController
├── DependencyInjection/
├── DisposableEmail/ # Wegwerf-Mail-Prüfung, Domain-Liste, Entities für Ausnahmen
├── Enforcement/ # Form-Type-Extension, Route-Listener, Event-Listener und -Registrar
├── Entity/ # ChannelSettings, CustomTarget, DisposableDomainOverride, GibberishLogEntry, RateLimitHit, UsedChallenge
├── Form/Type/ # Formulartypen für den Admin
├── Gibberish/ # Scorer, Trigramm-Tabellen, Build-Script
├── Guard/ # SubmissionGuard (steuert alle Prüfungen), Rejection-Enum
├── Menu/ # Eintrag in der Admin-Sidebar
├── Migrations/ # Sieben Doctrine-Migrationen
├── RateLimit/ # Rate-Limiter
├── Request/
├── Security/ # Secret-Verschlüsselung (libsodium)
├── Settings/ # Provider und Factory für Channel-Einstellungen
├── Submission/
├── Target/ # ProtectionTarget, Registry, eingebaute Schutzziele
├── Twig/ # Funktion mmd_altcha_widget()
├── Validator/
└── Verification/ # Proof-Prüfung und Replay-Schutz
config/
├── grids/ # Vier Admin-Grids
├── routes/ # admin.yaml, shop.yaml
└── twig_hooks/ # admin.yaml (Titelbereich und Aktionen im Admin), shop.yaml (Widget-Hooks der Storefront, u. a. mmd_altcha.widget*)
Entities
ChannelSettings, CustomTarget, DisposableDomainOverride, GibberishLogEntry, RateLimitHit und UsedChallenge: sechs Tabellen mit dem Präfix mmd_sylius_altcha_*. Es gibt sieben Migrationen:
- Die erste legt zwei Tabellen an (
used_challengeundrate_limit_hit). - Die zweite legt
channel_settingsan, die drittecustom_target. - Die vierte ergänzt
channel_settingsum den Schalter der Wegwerf-E-Mail-Prüfung und legtdisposable_domain_overridean. - Die fünfte ergänzt
channel_settingsnur um die Spalten der Gibberish-Prüfung. - Die sechste legt
gibberish_logan. - Die siebte ergänzt
channel_settingsnur um das Aufruf-Limit des Challenge-Endpunkts.
Betriebshinweise
- Client-Adresse. Rate-Limits gelten pro Formular und Client. IPv6-Clients werden pro /64 gezählt. Hinter einem Reverse-Proxy oder CDN musst du
framework.trusted_proxies(undtrusted_headers) von Symfony setzen, sonst teilen sich alle Besucher einen Zähler, und ein Ansturm einer einzelnen Person sperrt das Formular für alle. - Login im Checkout. Der Login wiederkehrender Kunden im Checkout (
sylius_shop_json_login_check) sendet über eigenes JavaScript und kann keine Lösung mitschicken. Er bekommt deshalb nur ein Rate-Limit (Schutzzielcheckout_login), nie eine Proof-of-Work-Prüfung. Das Limit zählt jeden Versuch, auch erfolgreiche, und gilt gemeinsam für alle hinter derselben IPv4-Adresse: Bei Kunden hinter einem Carrier-Grade-NAT oder einer Büro-Adresse das Rate-Limit erhöhen oder das Schutzziel abwählen. - Newsletter-Checkbox. Die Newsletter-Anmeldung über die Checkbox im Registrierungsformular wird nicht ein zweites Mal geprüft, weil die Registrierung ihre eigene Prüfung hat. Die öffentlichen Anmeldeformulare und die API werden geprüft.
- Zwei Schutzziele, ein Event. Nur ein Schutzziel setzt ein bestimmtes Event durch (das zuerst registrierte). Ein zweites Schutzziel mit demselben Event wird mit einer Warnung ignoriert, weil beide dieselbe Absendung prüfen würden und das zweite die Lösung schon verbraucht vorfände.
- Live-Components. Das Registrierungsformular ist eine Live-Component, die bei jeder Feldänderung neu validiert. Diese Neuberechnungen werden nicht geprüft. Eine eigene LiveAction, die ein geschütztes Formular tatsächlich absendet, wird geprüft und muss die Lösung mitschicken (Header
X-Altcha-Payloadoder Feldaltcha). - Logging. Kann die Datenbank nicht antworten (zum Beispiel bevor die Migration lief), laufen Formulare ungeschützt weiter, statt zu scheitern. Jedes Mal wird eine Warnung protokolliert.