From 60c6c10b0f58c0f040fada4e8a39e51e372e3499 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 02:59:14 +0000 Subject: [PATCH 01/10] SECURITY: Implement server-side SHA-256 verification for PoW Major security fix: The proof-of-work challenge was previously just trusting the client, allowing bots to bypass it by submitting random nonces without doing any work. Changes: - Added proper server-side SHA-256 verification using resty.sha256 - Server now verifies that sha256(challenge + nonce) has the required number of leading zeros based on pow_difficulty - Bots must now actually compute the proof-of-work Updated tests: - Added computeValidNonce() helper that actually computes valid nonces by brute force (for testing purposes) - testValidPowPassesChallenge now uses a real computed nonce Updated README to explicitly mention server-side verification. --- scripts/ddos_protection_challenge.README.md | 2 ++ scripts/ddos_protection_challenge.lua | 24 ++++++++++++++------ tests/ddos_protection_challenge.lua | 25 ++++++++++++++++++++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/scripts/ddos_protection_challenge.README.md b/scripts/ddos_protection_challenge.README.md index 3907106..cae5110 100644 --- a/scripts/ddos_protection_challenge.README.md +++ b/scripts/ddos_protection_challenge.README.md @@ -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. +**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 You can configure the module to protect only specific paths, which is useful for: diff --git a/scripts/ddos_protection_challenge.lua b/scripts/ddos_protection_challenge.lua index eb8b3f5..b3071f8 100644 --- a/scripts/ddos_protection_challenge.lua +++ b/scripts/ddos_protection_challenge.lua @@ -468,14 +468,24 @@ local function challengeCallback(cfg, state) local challenge_exists = state.tokens_dict:get("pow:" .. challenge) if challenge_exists then - -- Verify the hash using resty.sha256 or just trust client for now - -- In production, you'd want to verify: sha256(challenge + nonce) starts with N zeros - -- For simplicity, we'll trust the client solved it correctly - -- A more secure implementation would verify server-side - challenge_passed = true + -- Verify the proof-of-work server-side + local resty_sha256 = require("resty.sha256") + local str = require("resty.string") - -- Clean up the challenge - state.tokens_dict:delete("pow:" .. challenge) + local sha256 = resty_sha256:new() + 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 diff --git a/tests/ddos_protection_challenge.lua b/tests/ddos_protection_challenge.lua index 11bf984..d34b157 100644 --- a/tests/ddos_protection_challenge.lua +++ b/tests/ddos_protection_challenge.lua @@ -493,6 +493,26 @@ function TestDDoSProtectionChallengeQuestion:teardown() teardownNgx() 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 TestDDoSProtectionChallengePow = {} @@ -562,13 +582,16 @@ function TestDDoSProtectionChallengePow:testValidPowPassesChallenge() local test_challenge = 'test_pow_challenge' 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', {}) ngx.var.remote_addr = '192.168.4.2' ngx.var.request_method = 'POST' ngx._post_args = { return_to = '/api/test', challenge = test_challenge, - nonce = '12345' -- In reality, this would be computed + nonce = valid_nonce } onRequest() From fbe238d3c117e4ac65ed8510493d2946a6a5afe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 03:10:25 +0000 Subject: [PATCH 02/10] Add mock resty.sha256 and resty.string for testing The test environment doesn't have OpenResty libraries, so we need to provide mock implementations for testing. Created: - tests/mock_resty_sha256.lua: Uses system sha256sum command to compute SHA-256 hashes. Mimics the resty.sha256 API (new, update, final). - tests/mock_resty_string.lua: Implements to_hex() to convert binary strings to hexadecimal. Updated test.lua to preload these mocks so that when the module or tests require 'resty.sha256' or 'resty.string', they get our mock implementations instead. This allows the PoW verification tests to run and actually verify the SHA-256 proof-of-work. --- test.lua | 8 ++++++++ tests/mock_resty_sha256.lua | 35 +++++++++++++++++++++++++++++++++++ tests/mock_resty_string.lua | 13 +++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 tests/mock_resty_sha256.lua create mode 100644 tests/mock_resty_string.lua diff --git a/test.lua b/test.lua index 7152815..8179923 100644 --- a/test.lua +++ b/test.lua @@ -2,6 +2,14 @@ lu = require('luaunit') local rex = require('rex_pcre2') require('util') +-- Preload mock resty libraries for testing +package.preload['resty.sha256'] = function() + return require('tests.mock_resty_sha256') +end +package.preload['resty.string'] = function() + return require('tests.mock_resty_string') +end + function createNgx() local ngx = { status = nil diff --git a/tests/mock_resty_sha256.lua b/tests/mock_resty_sha256.lua new file mode 100644 index 0000000..4ba79cd --- /dev/null +++ b/tests/mock_resty_sha256.lua @@ -0,0 +1,35 @@ +-- Mock implementation of resty.sha256 for testing +-- Uses system sha256sum command since we don't have OpenResty libraries + +local sha256 = {} +sha256.__index = sha256 + +function sha256:new() + local obj = { + buffer = "" + } + setmetatable(obj, sha256) + return obj +end + +function sha256:update(data) + self.buffer = self.buffer .. data +end + +function sha256:final() + -- Use sha256sum command to compute hash + local handle = io.popen("echo -n '" .. self.buffer:gsub("'", "'\\''") .. "' | sha256sum") + local result = handle:read("*a") + handle:close() + + -- Parse hex string into binary + local hex = result:match("^(%x+)") + local binary = {} + for i = 1, #hex, 2 do + table.insert(binary, string.char(tonumber(hex:sub(i, i+1), 16))) + end + + return table.concat(binary) +end + +return sha256 diff --git a/tests/mock_resty_string.lua b/tests/mock_resty_string.lua new file mode 100644 index 0000000..474a21a --- /dev/null +++ b/tests/mock_resty_string.lua @@ -0,0 +1,13 @@ +-- Mock implementation of resty.string for testing + +local str = {} + +function str.to_hex(binary) + local hex = {} + for i = 1, #binary do + table.insert(hex, string.format("%02x", string.byte(binary, i))) + end + return table.concat(hex) +end + +return str From be059b26c1b96b2f473077c04ec584697f3e16a9 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 23 Nov 2025 00:30:17 -0300 Subject: [PATCH 03/10] make tests actually finish challenges quickly --- tests/ddos_protection_challenge.lua | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/ddos_protection_challenge.lua b/tests/ddos_protection_challenge.lua index d34b157..3fc8758 100644 --- a/tests/ddos_protection_challenge.lua +++ b/tests/ddos_protection_challenge.lua @@ -550,7 +550,7 @@ function TestDDoSProtectionChallengePow:setup() shared_dict_tokens = 'aproxy_tokens', protected_paths = {}, challenge_type = 'pow', - pow_difficulty = 4 + pow_difficulty = 1 } self.mod = require('scripts.ddos_protection_challenge') @@ -578,12 +578,22 @@ function TestDDoSProtectionChallengePow:testPowChallengeShown() lu.assertStrContains(ngx._say, '