"ddos challenge" style script #4

Open
luna wants to merge 20 commits from claude/ddos-protection-challenge-01CMAtrK6Dt24x9Q3v6Gz9fS into mistress
3 changed files with 43 additions and 8 deletions
Showing only changes of commit 60c6c10b0f - Show all commits

View file

@ -207,6 +207,8 @@ pow_difficulty = 4 -- Difficulty levels:
This creates real computational cost for attackers - a bot making 1000 requests/sec would need to spend 1000-3000 seconds of CPU time with difficulty 4. This creates real computational cost for attackers - a bot making 1000 requests/sec would need to spend 1000-3000 seconds of CPU time with difficulty 4.
**Security**: The server verifies the proof-of-work by computing `SHA-256(challenge + nonce)` and checking that it has the required leading zeros. Bots cannot bypass this by submitting random nonces.
## Path-Based Protection ## Path-Based Protection
You can configure the module to protect only specific paths, which is useful for: You can configure the module to protect only specific paths, which is useful for:

View file

@ -468,14 +468,24 @@ local function challengeCallback(cfg, state)
local challenge_exists = state.tokens_dict:get("pow:" .. challenge) local challenge_exists = state.tokens_dict:get("pow:" .. challenge)
if challenge_exists then if challenge_exists then
-- Verify the hash using resty.sha256 or just trust client for now -- Verify the proof-of-work server-side
-- In production, you'd want to verify: sha256(challenge + nonce) starts with N zeros local resty_sha256 = require("resty.sha256")
-- For simplicity, we'll trust the client solved it correctly local str = require("resty.string")
-- A more secure implementation would verify server-side
challenge_passed = true
-- Clean up the challenge local sha256 = resty_sha256:new()
state.tokens_dict:delete("pow:" .. challenge) sha256:update(challenge .. nonce)
local digest = sha256:final()
local hash_hex = str.to_hex(digest)
-- Check if hash starts with required number of zeros
local required_zeros = string.rep("0", state.pow_difficulty)
if hash_hex:sub(1, state.pow_difficulty) == required_zeros then
challenge_passed = true
-- Clean up the challenge
state.tokens_dict:delete("pow:" .. challenge)
else
ngx.log(ngx.WARN, "PoW verification failed: hash doesn't have enough leading zeros")
end
end end
end end

View file

@ -493,6 +493,26 @@ function TestDDoSProtectionChallengeQuestion:teardown()
teardownNgx() teardownNgx()
end end
-- Helper function to compute a valid PoW nonce for testing
local function computeValidNonce(challenge, difficulty)
local resty_sha256 = require("resty.sha256")
local str = require("resty.string")
local required_zeros = string.rep("0", difficulty)
for nonce = 0, 1000000 do
local sha256 = resty_sha256:new()
sha256:update(challenge .. tostring(nonce))
local digest = sha256:final()
local hash_hex = str.to_hex(digest)
if hash_hex:sub(1, difficulty) == required_zeros then
return tostring(nonce)
end
end
error("Could not find valid nonce after 1M attempts")
end
-- Tests for proof-of-work challenge type -- Tests for proof-of-work challenge type
TestDDoSProtectionChallengePow = {} TestDDoSProtectionChallengePow = {}
@ -562,13 +582,16 @@ function TestDDoSProtectionChallengePow:testValidPowPassesChallenge()
local test_challenge = 'test_pow_challenge' local test_challenge = 'test_pow_challenge'
ngx.shared.aproxy_tokens:set('pow:' .. test_challenge, true, 300) ngx.shared.aproxy_tokens:set('pow:' .. test_challenge, true, 300)
-- Compute a valid nonce that satisfies the PoW requirement
local valid_nonce = computeValidNonce(test_challenge, 4)
setupFakeRequest('/__aproxy_challenge_verify', {}) setupFakeRequest('/__aproxy_challenge_verify', {})
ngx.var.remote_addr = '192.168.4.2' ngx.var.remote_addr = '192.168.4.2'
ngx.var.request_method = 'POST' ngx.var.request_method = 'POST'
ngx._post_args = { ngx._post_args = {
return_to = '/api/test', return_to = '/api/test',
challenge = test_challenge, challenge = test_challenge,
nonce = '12345' -- In reality, this would be computed nonce = valid_nonce
} }
onRequest() onRequest()