Skip to content

Adaptive challenge count#220

Open
nhoway wants to merge 5 commits intotiagozip:mainfrom
nhoway:adaptive-challenge-count
Open

Adaptive challenge count#220
nhoway wants to merge 5 commits intotiagozip:mainfrom
nhoway:adaptive-challenge-count

Conversation

@nhoway
Copy link
Copy Markdown

@nhoway nhoway commented Apr 12, 2026

This PR introduces an adaptive challenge count mechanism that dynamically increases the number of proof-of-work challenges based on request frequency, providing progressive defense before hard rate-limiting kicks in.

Currently, challenge parameters are static per site key. The only defense against high request volume is rate-limiting, which is binary — requests are either allowed or blocked (429). This leaves a gap where an attacker can stay just below the rate limit while still automating solves at scale.

Adaptive challenge count fills this gap by gradually increasing computational cost as request frequency rises, making automation progressively more expensive without blocking legitimate users.

Copy link
Copy Markdown
Owner

@tiagozip tiagozip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't know if this would work as-is. we prefer having as little DB queries on the /challenge endpoint as possible as well, to reduce the damage of a DoS attack on it.

we also prefer not to store IPs in plain text anywhere, including in Redis. additionally, on per-ip mode, we're storing two keys for every single ip hitting the endpoint. this is reasonable at first but can quickly escalate with a big botnet.

Comment thread standalone/public/js/dashboard.js Outdated
@nhoway
Copy link
Copy Markdown
Author

nhoway commented Apr 14, 2026

Hi and thanks for the feedback.

I've reworked the implementation to minimize DB impact. Operations have been reduced to 2 INCR at most (1 per-IP + 1 global), down from 6 in the initial version. The previous design used separate INCR (write) + GET (read) calls: since INCR already returns the current value, the read is now eliminated entirely. EXPIRE is only called once per key per window. When adaptive is disabled (the default), zero DB calls are made. This is the same pattern used by the existing rate-limiter and I don't think it can be reduced further without moving to in-memory counters, which would diverge from the current architecture.

Regarding your concern about IP storage: IPs are no longer stored in plain text. Keys now use HMAC-SHA256(siteKey, ip) truncated to 16 hex chars, e.g. ac:{siteKey}:{hash}:{window}. The site key serves as the HMAC secret, so hashes can't be correlated across keys. This approach could also be applied to the existing rate-limiter if desired for better privacy on which I totally agree.

@nhoway nhoway requested a review from tiagozip April 14, 2026 18:27
@tiagozip
Copy link
Copy Markdown
Owner

sure, it'd be great to have the existing ratelimiter use redis too. we can probably drop it from a few places as well to make requests lighter.


When both global and per-IP tiers are configured, the **highest** resulting challenge count is used. The base challenge count (set in the **Main** section) is always the minimum.

Example configuration:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't really needed here.

<input type="range" id="cfgObfuscationLevel" min="1" max="10" value="${key.config.obfuscationLevel ?? 5}">
</div>
</div>
<h3 class="config-section-title" style="margin-top:16px">Adaptive challenge count</h3>
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont like the inline css very much but whatever it's fine

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, could you share a screenshot of this?

return { enabled, windowMs, tiers, globalTiers };
}

function tiersEqual(a, b) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't seem that this function is used anywhere outside of adaptiveConfigEquals. please inline it there.

Comment thread standalone/src/cap.js
}

function hashIp(ip, siteKey) {
return createHmac("sha256", siteKey).update(ip).digest("hex").slice(0, 16);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would ideally be async.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants