Skip to content

Anti-Bot Protection

POST /login is the one endpoint reachable before authentication, so it is the one credential-stuffing bots find. fetchly guards it with an invisible check that requires no user interaction — no puzzles, no images, no third-party service.

The two signals

1. Honeypot field

The login form renders a field named website, hidden from humans with CSS (.hp-field, positioned off-screen) but present in the DOM.

A real user never sees it and never fills it. Many automated form-fillers populate every input they find, so a non-empty value is a strong bot signal with effectively zero false positives.

2. Signed time-trap token

The login page embeds a token minted when the page is served, signed with FETCHLY_SECRET_KEY — the same anchor the session cookie uses, but under a distinct signing salt (fetchly-hidden-captcha) so a value minted for one can never be replayed as the other.

On submit the token must:

Requirement Catches
Be present with a valid signature A bot POSTing straight at the endpoint without loading the page
Not be older than 6 hours A stale form left open, or a harvested token reused much later
Not be younger than 1 second A form submitted implausibly fast after being served

Tokens are valid until expiry and are not single-use — a legitimate user who mistypes their password and retries is not punished for it.

Uniform rejection

Every failing signal — honeypot filled, token missing, signature invalid, token expired, token too fresh — produces the same message:

We couldn't verify your submission. Please reload the page and try again.

A caller can never learn which invisible check tripped, which is what keeps the checks from being tuned around one at a time.

Why not a CAPTCHA

Hidden check Third-party CAPTCHA
User interaction None Puzzles or image grids
External dependency None A vendor endpoint on every login
Privacy Nothing leaves the instance Client data sent to the vendor
Accessibility Unaffected A known barrier
Offline / air-gapped Works Breaks

The trade-off is that this stops opportunistic and scripted abuse, not a determined attacker who studies the form. That is what the login rate limit of 5/minute is for.

Implementation notes

No third-party dependency is involved. fetchly already hand-rolls HMAC-based token signing for sessions, so the anti-bot token follows the same base64url(payload + hmac) shape rather than pulling in a package for one small use.

Interaction with the secret key

Rotating FETCHLY_SECRET_KEY invalidates every token that was already handed out. Users with the login page open get the generic rejection and a reload fixes it — worth knowing before you rotate.

Layering

flowchart TD
    R[POST /login] --> RL[Rate limit: 5/minute per IP]
    RL --> C[CSRF double-submit check]
    C --> H[Honeypot field empty?]
    H --> T[Token: signed, 1 s – 6 h old?]
    T --> V[Constant-time credential comparison]
    V --> S[Issue session cookie]

Each layer stops a different class of caller: the rate limit stops volume, CSRF stops cross-site submission, the hidden check stops naive automation, and the constant-time comparison stops timing analysis.