Skip to content

cedric/spamtrap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spamtrap is a Rails gem that protects forms from spambots through three complementary mechanisms:

  • *Honeypot fields* — hidden textarea fields that real users never touch. Bots that fill them in are silently discarded with a 200 OK response.

  • *Cryptographic nonce* — an HMAC-SHA256 token binding each submission to a timestamp and client IP. Replayed or expired submissions are rejected before reaching the controller.

  • *Field name mutation* — all form field names are AES-128-GCM encrypted on every page render, so bots cannot target fields by recognizable names like email or body. The controller remaps them back transparently; no changes to application code are required.

Each feature is opt-in and can be used independently or combined.

Add the following to your Gemfile:

gem 'spamtrap'

Global defaults can be set in an initializer. Per-controller or per-action options always take precedence, including an explicit false that overrides a global true.

# config/initializers/spamtrap.rb
Spamtrap.nonce         = true       # require nonce on all spamtrap-protected actions
Spamtrap.nonce_timeout = 15.minutes # default 1800 seconds (30 minutes)
Spamtrap.mutate        = true       # mutate field names on all protected forms
Spamtrap.on_trap       = ->(reason:, request:) { ... }  # optional trap callback

These settings are read at request time, so changes take effect immediately without restarting the server.

An optional callback is invoked whenever the spamtrap fires — whether via the honeypot field or an invalid nonce. Use it for logging, metrics, rate-limiting, or any other custom behaviour.

Set a global callback in the initializer:

# config/initializers/spamtrap.rb
Spamtrap.on_trap = lambda do |reason:, request:|
  # reason is :honeypot or :nonce
  Rails.logger.warn "[Spamtrap] #{reason} trap fired from #{request.remote_ip} on #{request.path}"
  StatsD.increment('spamtrap.triggered', tags: ["reason:#{reason}"])
end

Or override per declaration (takes precedence over the global callback):

spamtrap :comment, only: :create, on_trap: ->(reason:, request:) {
  Honeybadger.notify("Spamtrap fired", context: { reason: reason, ip: request.remote_ip })
}

Callback arguments:

  • reason::honeypot when the hidden field was filled; :nonce when the HMAC nonce was missing, invalid, or expired.

  • request: — the ActionDispatch::Request object (IP, path, headers, etc.).

Exceptions raised inside the callback are rescued and logged; a broken callback never prevents the silent head 200 discard. If on_trap is not set, behaviour is unchanged from earlier versions.

A hidden textarea is added to the form with a deliberately enticing name. Real users never see or interact with it (it is hidden via CSS). Spambots, which auto-fill all visible and hidden fields, will populate it. When the form is submitted with a non-empty honeypot field, spamtrap silently discards the submission and returns a 200 OK response — indistinguishable from a successful submission to the bot.

Declare the controller actions you want protected:

class CommentsController < ApplicationController
  spamtrap :sarah_palin_walks_with_dinosaurs, only: %i(create update)
end

Add the honeypot field to your form using the f.spamtrap form builder helper:

- form_for @comment do |f|
  = f.spamtrap :sarah_palin_walks_with_dinosaurs, class: 'reindeer_jerky'
  %fieldset
    = f.label :body
    = f.text_area :body
  = f.submit

Hide it from real users with CSS:

.reindeer_jerky { display: none; }

The macro accepts an optional block for advanced use cases, such as swapping the honeypot with a real form parameter at runtime.

The nonce is an HMAC-SHA256 digest of the form submission timestamp and the client IP address, keyed with Rails.application.secret_key_base. Submissions with an expired or tampered nonce are silently discarded with a 200 response.

Enable globally via the initializer, or per-controller:

spamtrap :sarah_palin_walks_with_dinosaurs, nonce: true, only: %i(create update)

Add the nonce fields to your form:

- form_for @comment do |f|
  = f.spamtrap :sarah_palin_walks_with_dinosaurs, nonce: true, class: 'reindeer_jerky'

Override the timeout for a specific action (accepts seconds or an ActiveSupport duration):

spamtrap :field, nonce: true, nonce_timeout: 15.minutes, only: %i(create)

Note: because the nonce binds to the client IP, users whose IP address changes between page load and form submission (e.g. mobile network handoffs) will be rejected. This is an acceptable trade-off for spam protection but may not suit all deployments.

Every field is encrypted with AES-128-GCM using a random per-render salt, so field names are unrecognizable in HTML source and change on every page load. The controller decrypts and remaps them transparently before the action runs — no changes to params.require(...).permit(...) or any other controller code required.

Enable globally via the initializer, or per-controller:

spamtrap :sarah_palin_walks_with_dinosaurs, mutate: true, only: %i(create update)

Pass mutate: true to f.spamtrap in your view:

- form_for @comment do |f|
  = f.spamtrap :sarah_palin_walks_with_dinosaurs, mutate: true
  = f.label :body
  = f.text_area :body
  = f.label :email
  = f.email_field :email
  = f.submit

The rendered HTML will contain opaque encrypted field names (~28 characters each) instead of body, email, etc. The encryption key is derived from secret_key_base so only the originating server can decrypt submissions.

Mutation can be combined with the nonce for full replay and bot protection:

spamtrap :field, mutate: true, nonce: true, only: %i(create update)

= f.spamtrap :field, mutate: true, nonce: true

Mutation propagates automatically to fields_for nested builders, so nested attributes forms are mutated at every level without any additional configuration.

Model-backed forms remain fully supported: helpers still pre-populate from model values while rendering encrypted field names in the HTML.

(The MIT License)

Copyright © 2010-2026 Cedric Howe

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.

About

Spamtrap protects your application from spambots using honeypot fields, a cryptographic nonce, and field name mutation.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages