https://github.com/flucksite/lucky_honeypot
🍯 Simple invisible captcha spam protection for Lucky Framework apps.
https://github.com/flucksite/lucky_honeypot
bot-protection captcha crystal crystal-lang invisible-captcha lucky luckyframework security
Last synced: 13 days ago
JSON representation
🍯 Simple invisible captcha spam protection for Lucky Framework apps.
- Host: GitHub
- URL: https://github.com/flucksite/lucky_honeypot
- Owner: flucksite
- License: mit
- Created: 2025-11-21T17:47:56.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-04-26T16:57:39.000Z (about 2 months ago)
- Last Synced: 2026-04-26T18:25:21.594Z (about 2 months ago)
- Topics: bot-protection, captcha, crystal, crystal-lang, invisible-captcha, lucky, luckyframework, security
- Language: Crystal
- Homepage: https://codeberg.org/fluck/lucky_honeypot
- Size: 89.8 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# lucky_honeypot
[](https://codeberg.org/fluck/lucky_honeypot/actions?workflow=test.yml)
[](https://codeberg.org/fluck/lucky_honeypot/tags)
Simple invisible captcha spam protection for Lucky Framework apps.
> [!Note]
> The original repository is hosted at
> [https://codeberg.org/fluck/lucky_honeypot](https://codeberg.org/fluck/lucky_honeypot).
## How it works
This shard uses three techniques to catch spambots:
1. **Invisible fields**. Bots fill out every field, including ones hidden with CSS.
2. **Timing checks**. Bots submit forms instantly, humans need more time.
3. **Input signals**. Bots don't tend to trigger mouse, touch, scroll, keyboard, or focus events.
When either of the two first checks fail, the submission is quietly rejected.
The bot thinks it succeeded and moves on. The third one can be used to reject
submissions at a certain _human rating_ threshold, or to flag entries that may
be suspicious.
## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
lucky_honeypot:
codeberg: fluck/lucky_honeypot
```
2. Run `shards install`
## Usage
Require the shard in your `src/shards.cr`:
```crystal
require "lucky_honeypot"
```
Add the honeypot to the form you would like to protect:
```crystal
class SignUps::NewPage < AuthLayout
include LuckyHoneypot::Tag
def content
form_for SignUps::Create do
# ...
honeypot_input "user:website"
# ...
end
end
end
```
> [!TIP]
> The name of the honeypot input can be anything, but it's best to keep it in
> line with the rest of the fields in your form, so it looks believable.
Finally, configure the honeypot in the receiving action:
```crystal
class SignUps::Create < BrowserAction
include LuckyHoneypot::Pipe
honeypot "user:website"
post "/sign_up" do
# ...
end
end
```
That's it! Your form is now protected.
### Configuring the input
The `honeypot_input` is good to go out of the box, but there are some things to
consider. By default, it is rendered with a `style` attribute to make it
inaccessible for humans, so they don't accidentally fill it out. By passing a
class attribute, it is assumed that you're using your own CSS class to hide the
input:
```crystal
honeypot_input "user:website", class: "visually-hidden"
```
> [!IMPORTANT]
> When a `class` attribute is passed, the default `style` attribute won't be
> rendered. However, you can pass your own `style` attribute as well.
Aside from the special `class` attribute, you can pass any other attribute as
well:
```crystal
honeypot_input "user:website", data_purpose: "not for humans"
```
> [!NOTE]
> The `honeypot_input` macro method will also set a timestamp in the session to
> verify submission timing. Consider this when you add your own field instead
> of using this macro.
### Configuration options
```crystal
Habitat.configure do |settings|
# Default required delay between page load and form submission.
settings.default_delay = 2.seconds
# Disables the submission delay entirely; useful in test environments.
settings.disable_delay = false
# Default name for the signals input field
settings.signals_input_name = "honeypot_signals"
end
```
### Configuring the pipe
The `honeypot` macro does two things:
1. Ensure the declared honeypot field is not filled
2. Ensure the form was not submitted too quickly
The default timing for the form submission is 2 seconds, but that can be
configured:
```crystal
honeypot "user:website", wait: 5.seconds
# or
honeypot "user:website", wait: 1_500.milliseconds
```
> [!TIP]
> The ideal timing will depend on the length of your form. Do some testing to
> see how fast you can fill out the form, and use that as the baseline.
If a honeypot is filled or submitted too quickly, a `head 204 (No Content)` will
be returned. This is common behaviour for honeypots. The bot will assume the
submission was successful and move on to its next target. If you want to
handle it differently, can can pass a block with the desired response:
```crystal
honeypot "user:website" do
flash.info = "Moving on..."
redirect to: Home::Index, status: HTTP::Status::SEE_OTHER
end
```
Finally, you can also add multiple honeypots, each with their own timing and
HTTP handling:
```crystal
honeypot "user:website", wait: 5.seconds
honeypot "note" do
html Bot::IndexPage
end
```
## Detecting input signals
This shard comes with simple input signals detection built-in. It monitors
mouse movements, touch gestures, scroll triggers, keyboard input, and focus
events. If any of those are detected, it adds to the likeliness of human
interaction.
To track the input signals, add the `honeypot_signals` tag to your form:
```crystal
honeypot_signals
```
Similar to the `honeypot_input` tag, it accepts additional attributes:
```crystal
honeypot_signals data_some: "value"
```
The signals tag only tracks input signals and stores the result in a hidden
field that is submitted with the form. It is up to you what to do with the
information. You could for example use it to flag a record in case of a
suspicious submission:
```crystal
if LuckyHoneypot::Signals.human_rating(params.get("honeypot_signals")) < 0.2
# Do your thing
end
```
And if you want more information about which inputs were triggered:
```crystal
signals = LuckyHoneypot::Signals.from_json(params.get("honeypot_signals"))
signals.human_rating # a value between 0 (bot) and 1 (human)
signals.mouse? # if true, the mouse was moved
signals.touch? # if true, a touch gesture was detected
signals.scroll? # if true, a scroll was triggered
signals.keyboard? # if true, keyboard input was detected
signals.focus? # if true, input focus was triggered
```
> [!NOTE]
> The human rating is the fraction of the five signals (mouse, touch, scroll,
> keyboard, focus) that fired, so each one contributes `0.2`. A score of `0`
> is almost certainly a dumb bot, while `0.2` could be a sophisticated bot
> triggering a single signal, though a human filling out a short form at the
> top of the page may also land there.
>
> `0.4` is a reasonable rejection threshold: it still catches bots that fake
> one or two signals, but avoids false positives for autofill and password
> manager submissions, which often only trigger focus plus mouse or touch.
> Use `0.6` as a "flag as suspicious" threshold rather than a hard reject,
> since legitimate autofill users will frequently fall below it.
## Security considerations
This shard provides basic bot protection, but it should not be your only line of
defense. Here are few important points to consider:
- It's not foolproof and sophisticated bots can bypass honeypots
- Combine this with Lucky's built-in rate limiting feature
- For high-value forms, consider adding CAPTCHA or email verification
- The submission timestamp is stored in the session. If sessions are
compromised, an attacker could manipulate timing checks. Make sure your
session store is secure and uses signed cookies (Lucky's default)
- The timing check compares wall-clock timestamps, which makes it resilient to
timing attacks since the check is a simple threshold comparison
- This shard works alongside Lucky's built-in CSRF protection. The honeypot
fields are regular form inputs and do not interfere with CSRF tokens
For most use cases (contact forms, newsletter signups, etc.), this shard
provides excellent protection with zero user friction. By adding a honeypot,
you'll catch between 60% and 90% of all automated form submissions.
If you want protection from more sophisticated bots, have a look at the
[Prosopo shard](https://codeberg.org/fluck/prosopo) or the [hCaptcha
shard](https://codeberg.org/fluck/hcaptcha).
## Contributing
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
for our commit messages, so please adhere to that pattern.
1. Fork it ()
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'feat: add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## Contributors
- [Wout](https://codeberg.org/w0u7) - creator and maintainer