Security & Bot Protection
Protect your forms with rate limiting and Cloudflare Turnstile captcha.
Formlander includes multiple layers of protection against spam and bot submissions. You can use them individually or combine them for maximum protection.
Understanding Form Tokens
Section titled “Understanding Form Tokens”Each form in Formlander has a unique public token that’s embedded in your HTML. This is intentional, but tokens alone are not enough.
The Security Problem
Section titled “The Security Problem”Without additional protection, tokens can be copied:
<!-- Your legitimate form on example.com --><form action="https://your-formlander.com/forms/contact/submit?token=abc123"> <!-- ...fields... --></form>Attack scenario:
- Attacker inspects your HTML
- Copies the form with your token to
attacker.com - Can now spam submissions to your form from their site
The Solution: Origin Allowlisting
Section titled “The Solution: Origin Allowlisting”Formlander protects against this by checking the Origin or Referer header of each request.
How it works
Section titled “How it works”-
Configure allowed domains in form settings:
example.com, www.example.com, *.example.com -
Formlander checks every submission:
- ✅ Request from
example.com→ Allowed - ✅ Request from
www.example.com→ Allowed - ✅ Request from
app.example.com→ Allowed (wildcard) - ❌ Request from
attacker.com→ Rejected (403 Forbidden)
- ✅ Request from
-
Response on blocked origin:
{"ok": false,"error": "origin not allowed"}
Configuration Options
Section titled “Configuration Options”Allow all origins (default, backwards compatible):
- Leave “Allowed Origins” field empty
- ⚠️ Vulnerable to token copying attacks
Allow specific domain:
example.comAllow multiple domains:
example.com, example.org, myapp.comAllow domain + all subdomains:
*.example.comThis matches: app.example.com, staging.example.com, etc.
Combine specific + wildcard:
example.com, *.example.com, www.example.orgAdditional Token Security
Section titled “Additional Token Security”Why tokens are still public:
- ✅ Write-only access: Token only allows submitting to that specific form
- ✅ Cannot read submissions: Attackers can’t view existing data
- ✅ Form-specific: Each form has its own token, limiting blast radius
- ✅ Can be rotated: Regenerate tokens if compromised
With origin checking enabled:
- ✅ Token + correct origin required
- ✅ Prevents cross-site form abuse
- ✅ Same security model as Stripe, Plaid, and other API keys
Rate Limiting
Section titled “Rate Limiting”Formlander includes per-IP rate limiting to prevent spam floods and abuse.
How it works
Section titled “How it works”- Tracks submission rate per IP address per form
- Configurable time window and request limit
- Returns
429 Too Many Requestswhen exceeded
Environment configuration
Section titled “Environment configuration”Note: Rate limiting is currently configured per-form in the admin dashboard. The environment variables shown below are for reference and may be added in future versions.
# Global defaults (if supported in your version)FORMLANDER_RATE_MAX_REQUESTS=60 # Max requests per windowFORMLANDER_RATE_WINDOW_SECONDS=60 # Time window in secondsExample: 60 requests per minute
Section titled “Example: 60 requests per minute”If someone (or a bot) tries to submit more than 60 times in 60 seconds, they’ll be blocked:
{ "ok": false, "error": "rate limit exceeded"}Best practices
Section titled “Best practices”- Start conservative: 60 requests/minute is generous for contact forms
- Monitor usage: Check submission patterns in the dashboard
- Adjust per form: High-traffic forms may need higher limits
This example allows 10 submissions per IP address per 60 seconds. Adjust based on your needs:
- Contact forms: 3 submissions per 300 seconds (5 min)
- Newsletter signups: 1 submission per 3600 seconds (1 hour)
- Feedback forms: 5 submissions per 60 seconds
Per-form configuration
Section titled “Per-form configuration”Override global limits in the dashboard for individual forms. This lets you have stricter limits on public contact forms while allowing more frequent submissions on authenticated feedback forms.
Cloudflare Turnstile
Section titled “Cloudflare Turnstile”For additional protection, integrate Cloudflare Turnstile (a privacy-friendly CAPTCHA alternative) into your forms.
-
Get Turnstile keys from your Cloudflare dashboard
- Site Key (public)
- Secret Key (private)
-
Configure in Formlander admin:
- Log into your Formlander dashboard
- Navigate to Settings → Security
- Enable Turnstile protection
- Enter your Site Key and Secret Key
- Save changes
Note: Turnstile keys are stored in the database and managed through the admin interface, not environment variables.
-
Add Turnstile to your form:
<form action="https://your-domain.com/forms/contact/submit" method="post"><!-- Honeypot --><input type="text" name="__fl_hp" value="" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px;"><!-- Your form fields --><input type="text" name="name" required><input type="email" name="email" required><textarea name="message" required></textarea><!-- Cloudflare Turnstile widget --><div class="cf-turnstile" data-sitekey="your-site-key"></div><button type="submit">Send</button></form><!-- Load Turnstile script --><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> -
Formlander validates the Turnstile token server-side before accepting the submission.
Turnstile modes
Section titled “Turnstile modes”Cloudflare offers three modes:
- Managed (recommended): Shows CAPTCHA only when needed based on risk analysis
- Non-interactive: Invisible challenge that runs in the background
- Always visible: Traditional CAPTCHA behavior
Configure the mode in your Cloudflare Turnstile settings.
Recommended Security Stack
Section titled “Recommended Security Stack”For maximum protection, use all three layers:
- ✅ Honeypot - Catches simple bots (zero user friction)
- ✅ Rate limiting - Prevents spam floods (invisible to users)
- ✅ Turnstile (optional) - Blocks sophisticated bots (minimal friction)
Example configuration
Section titled “Example configuration”Recommended configuration
Section titled “Recommended configuration”For maximum protection, configure these settings:
Using config file (config.yaml):
submissionrateperhour: 120 # Max submissions per hour per IPUsing environment variables:
FORMLANDER_SUBMISSION_RATE_PER_HOUR=120For stricter limits:
# Contact forms: More restrictivesubmissionrateperhour: 10
# Newsletter signups: Very restrictivesubmissionrateperhour: 5
# Feedback forms: More lenientsubmissionrateperhour: 60Admin settings (Turnstile):
- Enable Turnstile in Settings → Security
- Configure your Cloudflare Site Key and Secret Key
HTML form:
<form action="https://your-domain.com/forms/contact/submit" method="post"> <!-- Honeypot --> <input type="text" name="__fl_hp" value="" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px;">
<label> Name <input type="text" name="name" required> </label>
<label> Email <input type="email" name="email" required> </label>
<label> Message <textarea name="message" required></textarea> </label>
<!-- Turnstile (if enabled) --> <div class="cf-turnstile" data-sitekey="1x00000000000000000000AA"></div>
<button type="submit">Send Message</button></form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>Monitoring Blocked Submissions
Section titled “Monitoring Blocked Submissions”How blocked submissions surface depends on which layer caught them:
- Honeypot — silently stored with
is_spam=trueand never forwarded to webhooks or email. The bot sees a normal 2xx response so it can’t tell it was trapped. Review or delete flagged submissions from the form’s submissions list. - Rate limits — rejected with
429 Too Many Requestsand recorded in the structured logs understorage/logs/. - Turnstile — failed challenges are rejected before storage and recorded in
storage/logs/.
Use these signals to identify spam patterns, adjust rate-limit thresholds, and fine-tune your settings.
Additional Security Tips
Section titled “Additional Security Tips”- Use HTTPS: Always serve forms over HTTPS to protect form data in transit
- Sanitize inputs: Formlander escapes HTML in the admin dashboard, but sanitize data in your webhooks
- Rotate secrets: Regularly update
FORMLANDER_SESSION_SECRETandFORMLANDER_ANON_SALT - Monitor logs: Watch for patterns in blocked submissions
- Update regularly: Keep Formlander updated to get the latest security patches