Skip to content

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.

Each form in Formlander has a unique public token that’s embedded in your HTML. This is intentional, but tokens alone are not enough.

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:

  1. Attacker inspects your HTML
  2. Copies the form with your token to attacker.com
  3. Can now spam submissions to your form from their site

Formlander protects against this by checking the Origin or Referer header of each request.

  1. Configure allowed domains in form settings:

    example.com, www.example.com, *.example.com
  2. 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.comRejected (403 Forbidden)
  3. Response on blocked origin:

    {
    "ok": false,
    "error": "origin not allowed"
    }

Allow all origins (default, backwards compatible):

  • Leave “Allowed Origins” field empty
  • ⚠️ Vulnerable to token copying attacks

Allow specific domain:

example.com

Allow multiple domains:

example.com, example.org, myapp.com

Allow domain + all subdomains:

*.example.com

This matches: app.example.com, staging.example.com, etc.

Combine specific + wildcard:

example.com, *.example.com, www.example.org

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

Formlander includes per-IP rate limiting to prevent spam floods and abuse.

  • Tracks submission rate per IP address per form
  • Configurable time window and request limit
  • Returns 429 Too Many Requests when exceeded

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.

Terminal window
# Global defaults (if supported in your version)
FORMLANDER_RATE_MAX_REQUESTS=60 # Max requests per window
FORMLANDER_RATE_WINDOW_SECONDS=60 # Time window in seconds

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"
}
  • 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

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.

For additional protection, integrate Cloudflare Turnstile (a privacy-friendly CAPTCHA alternative) into your forms.

  1. Get Turnstile keys from your Cloudflare dashboard

    • Site Key (public)
    • Secret Key (private)
  2. 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.

  3. 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>
  4. Formlander validates the Turnstile token server-side before accepting the submission.

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.

For maximum protection, use all three layers:

  1. Honeypot - Catches simple bots (zero user friction)
  2. Rate limiting - Prevents spam floods (invisible to users)
  3. Turnstile (optional) - Blocks sophisticated bots (minimal friction)

For maximum protection, configure these settings:

Using config file (config.yaml):

submissionrateperhour: 120 # Max submissions per hour per IP

Using environment variables:

Terminal window
FORMLANDER_SUBMISSION_RATE_PER_HOUR=120

For stricter limits:

# Contact forms: More restrictive
submissionrateperhour: 10
# Newsletter signups: Very restrictive
submissionrateperhour: 5
# Feedback forms: More lenient
submissionrateperhour: 60

Admin 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>

How blocked submissions surface depends on which layer caught them:

  • Honeypot — silently stored with is_spam=true and 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 Requests and recorded in the structured logs under storage/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.

  1. Use HTTPS: Always serve forms over HTTPS to protect form data in transit
  2. Sanitize inputs: Formlander escapes HTML in the admin dashboard, but sanitize data in your webhooks
  3. Rotate secrets: Regularly update FORMLANDER_SESSION_SECRET and FORMLANDER_ANON_SALT
  4. Monitor logs: Watch for patterns in blocked submissions
  5. Update regularly: Keep Formlander updated to get the latest security patches