From b589199cfd75f922155bf0b98e6b4f4538bcdc3d Mon Sep 17 00:00:00 2001 From: Thibault Koechlin Date: Tue, 24 Mar 2026 16:25:20 +0100 Subject: [PATCH] add support for challenge mode --- lib/crowdsec.lua | 28 +++++++++++++++++--- lib/plugins/crowdsec/challenge.lua | 41 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 lib/plugins/crowdsec/challenge.lua diff --git a/lib/crowdsec.lua b/lib/crowdsec.lua index 71fa6c3..bf1419b 100644 --- a/lib/crowdsec.lua +++ b/lib/crowdsec.lua @@ -8,6 +8,7 @@ local captcha = require "plugins.crowdsec.captcha" local flag = require "plugins.crowdsec.flag" local utils = require "plugins.crowdsec.utils" local ban = require "plugins.crowdsec.ban" +local challenge = require "plugins.crowdsec.challenge" local url = require "plugins.crowdsec.url" local metrics = require "plugins.crowdsec.metrics" local live = require "plugins.crowdsec.live" @@ -637,13 +638,21 @@ function csmod.AppSecCheck(ip) else status_code = ngx.HTTP_FORBIDDEN end + if remediation == "challenge" then + local appsec_response = { + body = response.user_body_content, + headers = response.user_headers, + cookies = response.user_cookies, + } + return ok, remediation, status_code, appsec_response, err + end elseif res.status == 401 then ngx.log(ngx.ERR, "Unauthenticated request to APPSEC") else ngx.log(ngx.ERR, "Bad request to APPSEC (" .. res.status .. "): " .. res.body) end - return ok, remediation, status_code, err + return ok, remediation, status_code, nil, err end @@ -655,6 +664,7 @@ function csmod.Allow(ip) local remediationSource = flag.BOUNCER_SOURCE local ret_code = nil local remediation = "" + local appsec_response = nil local ok = true local err = "" if runtime.conf["ENABLED"] ~= "false" then @@ -698,7 +708,7 @@ function csmod.Allow(ip) -- OR -- that user configured the remediation component to always check on the appSec (even if there is a decision for the IP) if is_appsec_enabled() and (ok == true or is_always_send_to_appsec()) then - local appsecOk, appsecRemediation, status_code, err = csmod.AppSecCheck(ip) + local appsecOk, appsecRemediation, status_code, appsec_resp, err = csmod.AppSecCheck(ip) if err ~= nil then ngx.log(ngx.ERR, "AppSec check: " .. err) end @@ -707,6 +717,7 @@ function csmod.Allow(ip) remediationSource = flag.APPSEC_SOURCE remediation = appsecRemediation ret_code = status_code + appsec_response = appsec_resp end end @@ -719,7 +730,7 @@ function csmod.Allow(ip) end -- if remediation is not supported, fallback - if remediation ~= "captcha" and remediation ~= "ban" then + if remediation ~= "captcha" and remediation ~= "ban" and remediation ~= "challenge" then remediation = runtime.fallback end end @@ -784,6 +795,17 @@ function csmod.Allow(ip) ban.apply(ret_code) return end + if remediation == "challenge" then + if appsec_response ~= nil then + ngx.log(ngx.DEBUG, "[Crowdsec] challenge '" .. ip .. "' (by " .. flag.Flags[remediationSource] .. ")") + challenge.apply(ret_code, appsec_response.body, appsec_response.headers, appsec_response.cookies) + return + else + ngx.log(ngx.ERR, "[Crowdsec] challenge remediation for '" .. ip .. "' but no response data, falling back to ban") + ban.apply(ret_code) + return + end + end -- if the remediation is a captcha and captcha is well configured if remediation == "captcha" and captcha_ok and ngx.var.uri ~= "/favicon.ico" then local previous_uri, flags = ngx.shared.crowdsec_cache:get("captcha_"..ip) diff --git a/lib/plugins/crowdsec/challenge.lua b/lib/plugins/crowdsec/challenge.lua new file mode 100644 index 0000000..532b183 --- /dev/null +++ b/lib/plugins/crowdsec/challenge.lua @@ -0,0 +1,41 @@ +local M = {_TYPE='module', _NAME='challenge.funcs', _VERSION='1.0-0'} + +--- Serve a challenge response from AppSec. +-- Sets the HTTP status, response headers, cookies, and body as provided by CrowdSec. +-- @param status_code number: HTTP status code (typically 200) +-- @param body string: the HTML body content to serve +-- @param headers table: map of header name -> list of values, e.g. {["Content-Type"] = {"text/html"}} +-- @param cookies table: list of Set-Cookie header value strings +function M.apply(status_code, body, headers, cookies) + ngx.status = status_code or ngx.HTTP_OK + + if headers ~= nil then + for name, values in pairs(headers) do + if type(values) == "table" then + if #values == 1 then + ngx.header[name] = values[1] + else + ngx.header[name] = values + end + else + ngx.header[name] = values + end + end + end + + if cookies ~= nil and #cookies > 0 then + if #cookies == 1 then + ngx.header["Set-Cookie"] = cookies[1] + else + ngx.header["Set-Cookie"] = cookies + end + end + + if body ~= nil then + ngx.say(body) + end + + ngx.exit(ngx.status) +end + +return M