Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/redis-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ jobs:
dockerfile: server-node/Dockerfile
- implementation: python
dockerfile: server-python/Dockerfile
- implementation: go-node
dockerfile: docker/Dockerfile
peerDockerfile: server-node/Dockerfile
- implementation: node-python
dockerfile: server-node/Dockerfile
peerDockerfile: server-python/Dockerfile
- implementation: python-go
dockerfile: server-python/Dockerfile
peerDockerfile: docker/Dockerfile
services:
redis:
image: redis:7-alpine
Expand All @@ -35,12 +44,19 @@ jobs:
node-version: '20'
- name: Build production image
run: docker build -f ${{ matrix.dockerfile }} -t fcaptcha-redis-test .
- name: Build peer image
run: |
if [ -n "${{ matrix.peerDockerfile }}" ]; then
docker build -f "${{ matrix.peerDockerfile }}" -t fcaptcha-redis-peer .
else
docker tag fcaptcha-redis-test fcaptcha-redis-peer
fi
- name: Start two replicas
env:
FCAPTCHA_SECRET: redis-conformance-secret
run: |
docker run -d --name fcaptcha-a --network host -e PORT=3101 -e REDIS_URL=redis://127.0.0.1:6379 -e FCAPTCHA_SECRET="$FCAPTCHA_SECRET" fcaptcha-redis-test
docker run -d --name fcaptcha-b --network host -e PORT=3102 -e REDIS_URL=redis://127.0.0.1:6379 -e FCAPTCHA_SECRET="$FCAPTCHA_SECRET" fcaptcha-redis-test
docker run -d --name fcaptcha-b --network host -e PORT=3102 -e REDIS_URL=redis://127.0.0.1:6379 -e FCAPTCHA_SECRET="$FCAPTCHA_SECRET" fcaptcha-redis-peer
for port in 3101 3102; do
for attempt in $(seq 1 30); do
curl -fsS "http://127.0.0.1:$port/health" && break
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ the project uses [Semantic Versioning](https://semver.org/) — with the caveat
that pre-2.0 it has used minor bumps for behaviour changes that a stricter
reading would call major. Read the **Breaking** entries rather than the number.

## [Unreleased]

### Security and fixes
- Reject mismatched signal commitments and unmet challenge minimum ages outside
the weighted score. Go consumes a verified proof even when its signal
commitment fails, matching Node and Python.
- Require the server-issued challenge nonce in npm engine signals, including
proofs without `signalsHash`. Missing or incorrect nonces withhold tokens and
produce the same diagnostic as the standalone server.
- Prevent Node request errors from terminating the server. Request validation
applies only to scoring POST routes; other methods retain normal routing.
- Make issued tokens unique and npm verification single-use. A full local Node
replay store reports `token_store_full` (Siteverify: `internal-error`) instead
of reporting an unused token as a replay.
- Fix expired and consumed challenge reuse and clean up browser session resources.

### Behavior changes
- Invisible form protection executes a fresh verification on every submission;
it no longer reuses the previous 60 seconds' score and single-use token.
- Direct npm `engine.verify()` callers receive an exception with `status: 400`
for malformed signal objects instead of a verification result.
- Fingerprint cardinality uses fixed 15-minute windows, capped at 16 members
per bucket, rather than sliding retention.
- Go and Python launcher access logs are off unless `FCAPTCHA_LOG_ACCESS=1`.
- Importing the Node server exports `{ app, start }`; programmatic consumers
call `start()` to open a listener. CLI startup is unchanged.

## [1.34.1] — 2026-08-30

### Fixed
Expand Down
14 changes: 10 additions & 4 deletions COMPLIANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ data from other sites.

## What is retained, and for how long

All of it is in process memory. **There is no database and nothing is written to
disk.** Restart the server and every value below is gone.
By default, security state is in process memory and disappears on restart.
When `REDIS_URL` is configured, it lives in Redis with expiry; operators control
Redis persistence and backups. FCaptcha itself does not write state to disk.

| State | Keyed on | Lifetime |
|---|---|---|
| Proof-of-work challenges | site key + IP | 5 minutes |
| Spent PoW solutions (replay guard) | solution hash | 10 minutes |
| Spent tokens (replay guard) | token signature | 10 minutes |
| Suspicion ledger (adaptive cost) | site key + IP | 15 minutes |
| Fingerprint cardinality | site + fingerprint / IP | 15-minute fixed windows; at most 16 members per bucket |
| Rate-limit counters | site key + IP | 60-second windows |
| Site-key state bounds | IP | 1 hour |
| Siteverify idempotency cache | caller-supplied key | 5 minutes |
Expand All @@ -95,7 +97,11 @@ hostname, action and customer data your integration supplied. Tokens are valid f

## Logging

Off by default. A self-hosted FCaptcha emits **no per-request logs** unless you
Off by default. Go and Python access logging can be enabled separately with
`FCAPTCHA_LOG_ACCESS=1`; it includes client addresses and request URLs, so its
retention and access controls are the operator’s responsibility.

A self-hosted FCaptcha emits **no verdict logs** unless you
turn them on.

`FCAPTCHA_LOG_VERDICTS=1` emits one JSON line per verification: score,
Expand Down Expand Up @@ -253,7 +259,7 @@ false-positive cost is measured in CI. Weigh that against your own requirement.

If your assessment says the fingerprinting is more than you want:

- `FCAPTCHA_LOG_VERDICTS` unset (the default) means no per-request logging.
- Leave `FCAPTCHA_LOG_VERDICTS` and `FCAPTCHA_LOG_ACCESS` unset to keep request logging off.
- Serve the widget from your own origin, which is the default, so no third-party
request is made at all.
- The signal collectors in `client/fcaptcha.js` are individually removable. Doing
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ FCaptcha.render('captcha', {
Overrides are HTML-escaped, so a string coming from a CMS or locale file cannot
turn the widget into an injection point.

For pages that mount and unmount widgets, call `FCaptcha.destroy(widgetId)` to
release listeners, sensors, timers, and workers. Invisible sessions expose
`session.destroy()`; the one-shot `FCaptcha.execute()` cleans itself up.

**Invisible Mode (Zero-Click)**

```html
Expand Down Expand Up @@ -794,6 +798,10 @@ Set `action` (and optionally `cdata`) when you request the token —
| `FCAPTCHA_LOG_VERDICTS` | Log one privacy-safe JSON line per `/api/verify` and `/api/score` (score, recommendation, category scores, and per-hit category/score/confidence). Omits IP, user agent, raw signals, and free-text detection reasons. For observability/tuning (`1`/`true`/`yes`/`on`) | off |
| `FCAPTCHA_LOG_VERDICTS_INCLUDE_RAW` | Also include the free-text detection `reason` in verdict logs. **Reasons can contain visitor-derived data** (reverse-DNS hostnames, UA/header fragments, form field ids) — only enable in trusted debugging contexts with no privacy obligations. Requires `FCAPTCHA_LOG_VERDICTS` | off |

Go and Python HTTP access logs are off by default. Set `FCAPTCHA_LOG_ACCESS=1` to enable
them; they include client addresses and request URLs. This is separate from
`FCAPTCHA_LOG_VERDICTS`.

### Trusted proxies

Every IP-derived check — datacenter ranges, Tor/VPN, rate limiting, token IP
Expand Down
Loading
Loading