Markus Michalski FAQ

ALTCHA: Technical Details

ALTCHA spam protection for Shopware 6.7 - check order as a diagram, registration protection, gibberish score and deprecated content validation, database tables, logging, security concept, plugin structure

Deutsche Version

Back to overview

ALTCHA Spam Protection: Technical Details

This documentation always refers to the latest published release.

Live demo: Try this and other plugins live, no installation, no risk: demo.markus-michalski.net

Check order

All three protected forms run through the same SpamProtectionFacade. The order is sorted by cost: cheap in-memory checks first, the cryptographic ALTCHA check after that, the rate limit with database access last.

graph TD
    A["Submission"] --> M{"Master switch on?"}
    M -->|"No"| OK["Form is processed"]
    M -->|"Yes"| B{"Honeypot field filled?"}
    B -->|"Yes"| R1["Rejected: honeypot_triggered"]
    B -->|"No"| C{"Faster than minimum fill time?"}
    C -->|"Yes"| R2["Rejected: too_fast"]
    C -->|"No"| D{"Disposable check on and match?"}
    D -->|"Yes"| R3["Rejected: disposable_email"]
    D -->|"No"| E{"Gibberish on and score too high?"}
    E -->|"Yes"| R4["Rejected: gibberish"]
    E -->|"No"| F{"Content validation on, not replaced, 2+ hits?"}
    F -->|"Yes"| R5["Rejected: content_validation_failed"]
    F -->|"No"| G{"ALTCHA valid?"}
    G -->|"No"| R6["Rejected: altcha_failed or altcha_missing"]
    G -->|"Yes"| H{"Rate limit ok?"}
    H -->|"No"| R7["Rejected: rate_limited"]
    H -->|"Yes"| I["Form is processed"]

A layer that is switched off is skipped. Content validation is additionally skipped as soon as gibberish detection or the disposable e-mail check is on. The rate limit counts only submissions that passed all layers before it.

Registration protection

Registration is protected through a route decorator, not an event. The check therefore runs before the account is created.

sequenceDiagram
    participant B as Browser
    participant D as RegisterRouteDecorator
    participant F as SpamProtectionFacade
    participant R as RegisterRoute
    B->>D: submit registration
    D->>D: protection active?
    D->>F: validate
    F-->>D: result
    alt spam detected
        D-->>B: rejected
    else no spam
        D->>R: register
        R-->>B: account created
    end

Contact form and newsletter run through event subscribers on contact_form.send and NewsletterRegisterEvent (priority 500), using the same facade.

Gibberish score

Gibberish detection scores every text field and takes the worst value. The score is the share of letters in conspicuous words, weighted by the amount of evidence:

Signal Description
Letter sequences The trigrams of a word are implausible in every language of the bundled table (German, English, French, Spanish, Italian, Dutch)
Keyboard runs Almost all letters of a word are neighbours in one keyboard row (asdfgh, qwertz)
Repetition A word consists of a short repeated unit (asdfasdf)

Skipped are URLs, e-mail addresses, code, words containing digits or symbols, and words in non-Latin scripts. Capitalised words count less (names), and short words rarely trigger a rejection on their own. A long lowercase word made of impossible letter sequences, however, can reach a high score by itself. The table is at src/Resources/data/gibberish-trigrams.php. If it is missing or corrupt, the check scores nothing and logs a single warning.

The deprecated content validation, in contrast, uses fixed rules (consonant ratio, keyboard sequences, repetition, consonant clusters, letter pairs) on first name, last name, street and city as well as the e-mail domain, and blocks from two conspicuous fields.

Database tables

mmd_altcha_rate_limit_log

Stores individual timestamps for a sliding time window.

Column Type Description
id BINARY(16) Primary key
ip_hash CHAR(64) HMAC-SHA256 hash of the IP address
action VARCHAR(50) contact_form, newsletter, registration, challenge
sales_channel_id BINARY(16) optional, foreign key to sales_channel
requested_at DATETIME(3) Time of the request

Indexes: idx_rate_check (ip_hash, action, requested_at), idx_rate_check_channel (adds sales_channel_id) and idx_cleanup (requested_at).

mmd_altcha_used_challenge

Prevents replay attacks.

Column Type Description
challenge_hash CHAR(64) Primary key, nonce of the challenge
expires_at DATETIME(3) Expiry time
created_at DATETIME(3) Creation time

INSERT IGNORE claims a challenge atomically: only the first writer wins.

The disposable-domain list and the trigram table are files in the plugin or under var/mmd_altcha/, not in the database.

Logging and rejection reasons

Blocked attempts appear in the Shopware log, never with the submitted text:

[INFO] Spam attempt blocked {"reason":"honeypot_triggered","ip_hash":"...","sales_channel_id":"..."}
[INFO] Registration spam attempt blocked {"reason":"disposable_email","ip_hash":"...","sales_channel_id":"..."}
reason Meaning
honeypot_triggered Honeypot field was filled
too_fast Submission faster than the minimum fill time
disposable_email Disposable e-mail address detected
gibberish Gibberish score reaches the reject threshold
content_validation_failed Two or more fields conspicuous in the deprecated content validation
altcha_missing No ALTCHA payload in the request
altcha_failed ALTCHA solution invalid, expired or already used
rate_limited IP rate limit exceeded

Security concept

graph TD
    A["APP_SECRET"] -->|"signs"| B["ALTCHA challenge"]
    A -->|"signs"| C["Form token"]
    A -->|"hashes"| D["IP addresses"]
    B -->|"solved and verified"| E["Nonce is claimed"]
    E -->|"INSERT IGNORE"| F["mmd_altcha_used_challenge"]
    D --> G["mmd_altcha_rate_limit_log"]
    H["Task rate_limit_cleanup"] -->|"every 6 hours"| F
    H --> G

Plugin structure

MmdAltchaIntegration/
β”œβ”€β”€ Altcha/                              # bundled altcha-org/altcha library (MIT)
β”œβ”€β”€ Command/UpdateDisposableDomainsCommand   # mmd:altcha:update-disposable-domains
β”œβ”€β”€ Controller/Api/BlockedDomainsController  # GET /api/_action/mmd-altcha/blocked-domains
β”œβ”€β”€ Core/
β”‚   β”œβ”€β”€ Checkout/Customer/SalesChannel/RegisterRouteDecorator
β”‚   └── Content/RateLimitLog/            # entity definition, entity, collection
β”œβ”€β”€ DisposableEmail/                     # checker, list reader, updater, catalog, parser
β”œβ”€β”€ Exception/                           # SpamDetectedException, SpamRejection
β”œβ”€β”€ Gibberish/                           # guard, scorer, score, TrigramModel
β”œβ”€β”€ Migration/                           # rate-limit and used-challenge tables
β”œβ”€β”€ ScheduledTask/                       # rate_limit_cleanup, disposable_domains_refresh
β”œβ”€β”€ Service/                             # challenge, honeypot, form token, rate limit, facade
β”œβ”€β”€ Storefront/Controller/AltchaChallengeController
β”œβ”€β”€ Subscriber/                          # ContactFormSubscriber, NewsletterSubscriber
└── Twig/AltchaExtension                 # Twig functions for templates