Packages

Invisible honeypot spam protection for any Plug-based Elixir app.

Current section

Files

Jump to
honeytrap README.md
Raw

README.md

# Honeytrap

[![CI](https://codeberg.org/fluck/honeytrap/actions/workflows/ci.yml/badge.svg)](https://codeberg.org/fluck/honeytrap/actions?workflow=ci.yml)
[![Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcodeberg.org%2Fapi%2Fv1%2Frepos%2Ffluck%2Fhoneytrap%2Ftags&query=%24%5B0%5D.name&label=version)](https://codeberg.org/fluck/honeytrap/tags)

Invisible honeypot spam protection for any Plug-based Elixir app.

Bots fill every form field they can find. Honeytrap adds a hidden field that
real users never touch. It also measures how fast the form was submitted and
collects a few passive interaction signals. If any of those look wrong, the
request is flagged. You decide what to do with the flag.

## Install

Add `honeytrap` to your deps in `mix.exs`:

```elixir
def deps do
  [
    {:honeytrap, "~> 0.1.0"}
  ]
end
```

## Usage

### 1. Arm the trap

Set a timestamp on the session when you render the form. Do this in your
controller action.

```elixir
def new(conn, _params) do
  conn = Honeytrap.arm(conn, :website)
  render(conn, :new)
end
```

### 2. Render the fields

Add the honeypot field and the signals field to your form template.

```heex
<form action={~p"/contact"} method="post">
  <%= Honeytrap.field(:website) %>
  <%= Honeytrap.signals_field() %>

  <label>Message <textarea name="message"></textarea></label>
  <button type="submit">Send</button>
</form>
```

> [!NOTE]
> The honeypot field is hidden with inline styles by default. Pass a `:class`
> or `:style` to use your own hiding.

### 3. Plug the check into your pipeline

```elixir
pipeline :submissions do
  plug Honeytrap.Plug, fields: [:website]
end
```

On matching requests the plug puts a result in `conn.assigns.honeytrap`:

```elixir
%{
  bot: true,
  reasons: [{:filled, :website}, {:too_fast, :website}],
  human_rating: 0.2
}
```

### 4. Act on the result

Honeytrap does not halt the pipeline. Your action decides what to do.
That way you keep an escape hatch for false positives.

```elixir
def create(conn, params) do
  if conn.assigns.honeytrap.bot do
    render(conn, :thanks)
  else
    Contact.deliver(params)
    render(conn, :thanks)
  end
end
```

## Configuration

Pass options to the plug directly:

- `:fields` (required). List of honeypot field names to check. Set per plug
  call, since different forms use different fields.

These options can also be set globally via `Application` env under the
`:honeytrap` key. Per-plug values override the global default.

- `:default_delay` (default `2.0`). Minimum seconds between arm and submit.
- `:disable_delay` (default `false`). Skip the elapsed time check.
- `:minimum_human_rating` (default `nil`). If set, flags submissions below this
  score.
- `:signals_input_name` (default `"honeytrap_signals"`). Name of the signals
  input.

Example app config:

```elixir
# config/config.exs
config :honeytrap, default_delay: 3.0
```

## How it works

Three independent checks:

1. **Filled honeypot**. Real users cannot see the field. Bots often fill it
   anyway.
2. **Elapsed time**. Real users take at least a second or two. Instant submits
   are suspicious.
3. **Human signals**. A tiny script watches for mouse, touch, scroll, keyboard,
   and focus events on the form. The result is packed into a hidden JSON blob.
   You can act on the rating or ignore it.

Session storage is used for the arming timestamp. No cookies beyond
what you already have.

## License

MIT.