commit 16c604ee1eac82c830f8dd4eae8d47add71aae3f Author: potatso Date: Tue Jun 27 22:50:17 2023 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8637043 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +1 . ffi 调用 C动态库中函数时,如果函数时有类似 char** (char的指针的指针)类型的的参数时,lua 代码中,按如下方式申明变量,并分配好内存空间: + +local initValue = "这是初始值" +local inLen = string.len(initValue) +local inStr = ffi.new("char[?]", inLen + 2, initValue) +local inPtr = ffi.new("char*[1]", inStr); +-- 这里的 inPtr 就可以传入 C 函数中了,对应是参数类型应是 char**, 如果有C函数中对此参数有变更, +-- 则可以在lua中获取到返回值 + +2. 如何集成到openresty里? + +因为cgo的多线程会死锁 https://www.v2ex.com/t/568117 +所以必须在init_worker阶段加载cgo代码 必须这样配置 + +``` + init_worker_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_init() + coraza.rules_add([[SecRule REQUEST_HEADERS:User-Agent "Mozilla" "phase:1, id:3,drop,status:452,log,msg:'Blocked User-Agent'"]]) + } +``` +其他一切正常 +``` + location /t { + access_by_lua_block { + local coraza = require "resty.coraza" + coraza.do_access_filter() + } + + header_filter_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_header_filter() + } + + log_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_free() + } +``` + +3. 因为在调用go的时候,go并没有转换`char *`到go中string,只是单纯做了类型转换。也就是说,在调用期间一定要保证lua字符串不会被free,不然go中很有可能产生UAF漏洞。但是好在lua vm会自动管理内存,这点不必担心 \ No newline at end of file diff --git a/lib/resty/coraza.lua b/lib/resty/coraza.lua new file mode 100644 index 0000000..ffcfb4b --- /dev/null +++ b/lib/resty/coraza.lua @@ -0,0 +1,118 @@ +local log = require "resty.coraza.log" +local request = require "resty.coraza.request" +local coraza = require "resty.coraza.coraza" +local consts = require "resty.coraza.constants" + +local nlog = ngx.log +local ngx_var = ngx.var +local ngx_ctx = ngx.ctx +local ngx_req = ngx.req +local fmt = string.format + +local debug_fmt = log.debug_fmt +local err_fmt = log.err_fmt +local warn_fmt = log.warn_fmt + +local _M = { + _VERSION = '1.0.0' +} + +function _M.do_init() + _M.waf = coraza.new_waf() +end + +function _M.rules_add_file(file) + coraza.rules_add_file(_M.waf, file) +end + +function _M.rules_add(directives) + coraza.rules_add(_M.waf, directives) +end + +function _M.do_access_filter() + -- each connection will be created a transaction + local transaction = coraza.new_transaction(_M.waf) + ngx_ctx.transaction = transaction + + coraza.process_connection(transaction, ngx_var.remote_addr, ngx_var.remote_port, + ngx_var.server_addr, ngx_var.server_port) + + -- process uri + coraza.process_uri(transaction, ngx_var.request_uri, ngx_req.get_method(), ngx_var.server_protocol) + + -- process http get args.The coraza_process_uri function recommends using AddGetRequestArgument to add get args + request.build_and_process_get_args(transaction) + + -- process http req headers + request.build_and_process_header(transaction) + + -- process http req body if has + request.build_and_process_body(transaction) + + ngx_ctx.action, ngx_ctx.status_code = coraza.intervention(transaction) + _M.do_handle() +end + +function _M.do_free() + local transaction = ngx_ctx.transaction + if transaction ~= nil then + nlog(debug_fmt("transaction %s is freed by coraza_free_transaction", ngx_ctx.request_id)) + ngx_ctx.transaction = nil + coraza.free_transaction(transaction) + end +end + +function _M.do_handle() + -- transaction is interrupted by policy, be free firstly. + -- If request has disrupted by coraza, the transaction is freed and set to nil. + -- Response which was disrupted doesn't make sense. + if ngx_ctx.action ~= nil and ngx_ctx.transaction ~= nil then + nlog(warn_fmt([[Transaction %s request: "%s" is interrupted by policy. Action is %s]], + ngx_ctx.request_id, ngx_var.request, ngx_ctx.action)) + if ngx_ctx.action == "drop" then + ngx.status = ngx_ctx.status_code + local ok, msg = pcall(ngx.say, fmt(consts.BLOCK_CONTENT_FORMAT, ngx_ctx.status_code)) + if ok == false then + nlog(err_fmt(msg)) + end + return ngx.exit(ngx.status) + -- TODO: disrupted by more action + --elseif ngx_ctx.action == "deny" then + -- ngx.status = ngx_ctx.status_code + -- -- NYI: cannot call this C function (yet) + -- -- ngx.header.content_type = consts.BLOCK_CONTENT_TYPE + -- ngx.say(fmt(consts.BLOCK_CONTENT_FORMAT, ngx_ctx.status_code)) + -- return ngx.exit(ngx.status) + end + end +end + +function _M.do_header_filter() + if ngx_ctx.action ~= nil then + -- If request was interrupted by coraza at access_by_lua phrase, the ngx_ctx.transaction will be set nil. + -- We can bypass the check. + nlog(debug_fmt("Transaction %s has been disrupted at request phrase. ignore", ngx_ctx.request_id)) + return + end + local h = ngx.resp.get_headers(0, true) + for k, v in pairs(h) do + coraza.add_response_header(ngx_ctx.transaction, k, v) + end + -- copy from https://github.com/SpiderLabs/ModSecurity-nginx/blob/d59e4ad121df702751940fd66bcc0b3ecb51a079/src/ngx_http_modsecurity_header_filter.c#L527 + coraza.process_response_headers(ngx_ctx.transaction, ngx.status, "HTTP 1.1") + + -- TODO: add http response body to coraza.append_response_body. Openresty can't disrupt the body_filter phrase + --local resp_body = string.sub(ngx.arg[1], 1, 1000) + --ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body + --if ngx.arg[2] then + -- ngx.var.resp_body = ngx.ctx.buffered + --end + -- + --coraza.append_response_body(ngx_ctx.transaction, ngx.ctx.buffered) + --coraza.process_response_body(ngx_ctx.transaction) + + ngx_ctx.action, ngx_ctx.status_code = coraza.intervention(ngx_ctx.transaction) + _M.do_handle() +end + +return _M diff --git a/lib/resty/coraza/constants.lua b/lib/resty/coraza/constants.lua new file mode 100644 index 0000000..0317f83 --- /dev/null +++ b/lib/resty/coraza/constants.lua @@ -0,0 +1,14 @@ +local t = {} + + +t.MODE_OFF = "off" +t.MODE_BLOCK = "block" +t.MODE_MONITOR = "monitor" + + +t.NGX_HTTP_HEADER_PREFIX = "http_" + +t.BLOCK_CONTENT_TYPE = "application/json" +t.BLOCK_CONTENT_FORMAT = [[{"code": %d, "message": "This connection was blocked by Coroza!"}]] + +return t diff --git a/lib/resty/coraza/coraza.lua b/lib/resty/coraza/coraza.lua new file mode 100644 index 0000000..7ce52be --- /dev/null +++ b/lib/resty/coraza/coraza.lua @@ -0,0 +1,319 @@ +--- +--- Generated by EmmyLua(https://github.com/EmmyLua) +--- Created by mac. +--- DateTime: 2023/6/14 09:50 +--- +local ffi = require "ffi" +local log = require "resty.coraza.log" + +local nlog = ngx.log +local ngx_var = ngx.var +local ngx_ctx = ngx.ctx + +local err_fmt = log.err_fmt +local debug_fmt = log.debug_fmt + +local cast_to_c_char = function(str) + return ffi.cast("char *", str) +end + +local ok, coraza = pcall(ffi.load, "/usr/local/lib/libcoraza.dylib") +if ok ~= true then + ok, coraza = pcall(ffi.load, "libcoraza.so") + if ok ~= true then + nlog(log.err_fmt("Unable to load libcoraza, exiting! %s\n----", debug.traceback())) + return + end +end + +ffi.cdef [[ +typedef struct coraza_intervention_t +{ + char *action; + char *log; + char *url; + int status; + int pause; + int disruptive; +} coraza_intervention_t; + +typedef uint64_t coraza_waf_t; +typedef uint64_t coraza_transaction_t; + + +typedef void (*coraza_log_cb) (const void *); +void send_log_to_cb(coraza_log_cb cb, const char *msg); + +/*not used api/ not implement api*/ +extern int coraza_update_status_code(coraza_transaction_t t, int code); +extern int coraza_rules_count(coraza_waf_t w); +extern int coraza_rules_merge(coraza_waf_t w1, coraza_waf_t w2, char** er); +extern void coraza_set_log_cb(coraza_waf_t waf, coraza_log_cb cb); + + +/*initialize phrase/ init_worker_by_lua*/ +extern coraza_waf_t coraza_new_waf(); +extern int coraza_rules_add_file(coraza_waf_t w, char* file, char** er); +extern int coraza_rules_add(coraza_waf_t w, char* directives, char** er); + +/*http request phrase*/ +extern coraza_transaction_t coraza_new_transaction(coraza_waf_t waf, void* logCb); +extern coraza_transaction_t coraza_new_transaction_with_id(coraza_waf_t waf, char* id, void* logCb); +extern int coraza_process_connection(coraza_transaction_t t, char* sourceAddress, int clientPort, char* serverHost, int serverPort); +extern int coraza_process_uri(coraza_transaction_t t, char* uri, char* method, char* proto); +extern int coraza_add_get_args(coraza_transaction_t t, char* name, char* value); +extern int coraza_add_request_header(coraza_transaction_t t, char* name, int name_len, char* value, int value_len); +extern int coraza_process_request_headers(coraza_transaction_t t); +extern int coraza_append_request_body(coraza_transaction_t t, unsigned char* data, int length); +extern int coraza_request_body_from_file(coraza_transaction_t t, char* file); +extern int coraza_process_request_body(coraza_transaction_t t); + +/*http response phrase*/ +extern int coraza_add_response_header(coraza_transaction_t t, char* name, int name_len, char* value, int value_len); +extern int coraza_process_response_headers(coraza_transaction_t t, int status, char* proto); +extern int coraza_append_response_body(coraza_transaction_t t, unsigned char* data, int length); +extern int coraza_process_response_body(coraza_transaction_t t); + +/* end */ +extern int coraza_process_logging(coraza_transaction_t t); +extern int coraza_free_transaction(coraza_transaction_t t); +extern int coraza_free_intervention(coraza_intervention_t* it); +extern int coraza_free_waf(coraza_waf_t t); +extern coraza_intervention_t* coraza_intervention(coraza_transaction_t tx); + +]] + +local _M = { + _VERSION = '1.0.0' +} +-- global variable to store error value +local err_Str = "error" +local err_in_Ptr = ffi.new("char[?]", #err_Str + 2, err_Str) +local err_Ptr = ffi.new("char*[1]", err_in_Ptr); + +function _M.new_waf() + -- extern coraza_waf_t coraza_new_waf(); + local waf = coraza.coraza_new_waf() + nlog(debug_fmt("Success to creat new waf")) + return waf +end + +function _M.rules_add_file(waf, conf_file) + -- extern int coraza_rules_add_file(coraza_waf_t w, char* file, char** er); + local code = coraza.coraza_rules_add_file(waf, cast_to_c_char(conf_file), err_Ptr) + if code == 0 then + nlog(err_fmt(ffi.string(err_Ptr[0]))) + else + nlog(debug_fmt("Success to load rule file with %s", conf_file)) + end +end + +function _M.rules_add(waf, rule) + -- extern int coraza_rules_add(coraza_waf_t w, char* directives, char** er); + local code = coraza.coraza_rules_add(waf, cast_to_c_char(rule), err_Ptr) + if code == 0 then + nlog(err_fmt(ffi.string(err_Ptr[0]))) + else + nlog(debug_fmt("Success to load rule with %s", rule)) + end +end + +function _M.new_transaction(waf) + -- a transaction represent a http request and reponse.It should free when + -- end of process.If there is a memory leak issue, you should focus on + -- checking whether the transaction objects are correctly released or not. + -- In end of process, or intervention. + -- extern coraza_transaction_t coraza_new_transaction(coraza_waf_t waf, void* logCb); + local res = coraza.coraza_new_transaction(waf, nil) + ngx_ctx.request_id = ngx_var.request_id + nlog(debug_fmt("Success to creat new transaction id %s", ngx_ctx.request_id)) + return res +end + +function _M.process_connection(transaction, sourceAddress, clientPort, serverHost, serverPort) + -- extern int coraza_process_connection(coraza_transaction_t t, char* sourceAddress, int clientPort, + -- char* serverHost, int serverPort); + local res = coraza.coraza_process_connection(transaction, cast_to_c_char(sourceAddress), + tonumber(clientPort), cast_to_c_char(serverHost), tonumber(serverPort)) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_process_connection with " .. + "sourceAddress:%s clientPort:%s serverHost:%s serverPort:%s", + ngx_ctx.request_id, sourceAddress, clientPort, serverHost, serverPort)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_process_connection with " .. + "sourceAddress:%s clientPort:%s serverHost:%s serverPort:%s", + ngx_ctx.request_id, sourceAddress, clientPort, serverHost, serverPort)) + end +end + +function _M.process_uri(transaction, uri, method, proto) + -- This function won't add GET arguments, they must be added with AddArgument + -- extern int coraza_process_uri(coraza_transaction_t t, char* uri, char* method, char* proto); + local res = coraza.coraza_process_uri(transaction, cast_to_c_char(uri), + cast_to_c_char(method), cast_to_c_char(proto)) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_process_uri with %s %s %s", + ngx_ctx.request_id, ngx_ctx.request_id, method, uri, proto)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_process_uri with %s %s %s", + ngx_ctx.request_id, method, uri, proto)) + end +end + +function _M.add_request_header(transaction, header_name, header_value) + -- extern int coraza_add_request_header(coraza_transaction_t t, char* name, int name_len, + -- char* value, int value_len); + local res = coraza.coraza_add_request_header(transaction, cast_to_c_char(header_name), #header_name, + cast_to_c_char(header_value), #header_value) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_add_request_header with %s:%s", + ngx_ctx.request_id, header_name, header_value)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_add_request_header with %s:%s", + ngx_ctx.request_id, header_name, header_value)) + end +end + +function _M.add_get_args(transaction, header_name, header_value) + -- extern int coraza_add_get_args(coraza_transaction_t t, char* name, char* value); + local res = coraza.coraza_add_get_args(transaction, cast_to_c_char(header_name), + cast_to_c_char(header_value)) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_add_get_args with %s:%s", + ngx_ctx.request_id, header_name, header_value)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_add_get_args with %s:%s", + ngx_ctx.request_id, header_name, header_value)) + end +end + +function _M.process_request_headers(transaction) + -- extern int coraza_process_request_headers(coraza_transaction_t t); + local res = coraza.coraza_process_request_headers(transaction) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_process_request_headers", + ngx_ctx.request_id)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_process_request_headers", + ngx_ctx.request_id)) + end +end + +function _M.intervention(transaction) + -- extern coraza_intervention_t* coraza_intervention(coraza_transaction_t tx); + local intervention = coraza.coraza_intervention(transaction) + if intervention ~= nil then + local action = ffi.string(intervention.action) + local status_code = tonumber(intervention.status) + --free intervention to avoid memory leak + coraza.coraza_free_intervention(intervention) + nlog(debug_fmt("Transaction %s disrupted with status %s action %s", + ngx_ctx.request_id, status_code, action)) + return action, status_code + else + nlog(debug_fmt("Failed to disrupt transaction %s", ngx_ctx.request_id)) + return nil, nil + end + +end + +function _M.free_transaction(transaction) + -- extern int coraza_free_transaction(coraza_transaction_t t); + local res = coraza.coraza_free_transaction(transaction) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_free_transaction", + ngx_ctx.request_id)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_free_transaction", + ngx_ctx.request_id)) + end +end + +function _M.append_request_body(transaction, body) + -- extern int coraza_append_request_body(coraza_transaction_t t, unsigned char* data, int length); + local res = coraza.coraza_append_request_body(transaction, cast_to_c_char(body), #body) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_append_request_body with %s", + ngx_ctx.request_id, body)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_append_request_body with %s", + ngx_ctx.request_id, body)) + end +end + +function _M.request_body_from_file(transaction, file_path) + -- extern int coraza_request_body_from_file(coraza_transaction_t t, char* file); + -- return 0 if success, otherwish return 1 + local res = coraza.coraza_request_body_from_file(transaction, cast_to_c_char(file_path)) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_request_body_from_file with %s", + ngx_ctx.request_id, file_path)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_request_body_from_file with %s", + ngx_ctx.request_id, file_path)) + end +end + +function _M.process_request_body(transaction) + -- extern int coraza_process_request_body(coraza_transaction_t t); + local res = coraza.coraza_process_request_body(transaction) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_process_request_body", + ngx_ctx.request_id)) + else + nlog(debug_fmt("Transaction %s uccess to invoke coraza_process_request_body", + ngx_ctx.request_id)) + end +end + +-- for processing response + +function _M.process_response_headers(transaction, status_code, proto) + -- extern int coraza_process_response_headers(coraza_transaction_t t, int status, char* proto); + local res = coraza.coraza_process_response_headers(transaction, tonumber(status_code), cast_to_c_char(proto)) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_process_response_headers with %s %s", + ngx_ctx.request_id, status_code, proto)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_process_response_headers with %s %s", + ngx_ctx.request_id, status_code, proto)) + end +end + +function _M.add_response_header(transaction, header_name, header_value) + -- extern int coraza_add_response_header(coraza_transaction_t t, char* name, + -- int name_len, char* value, int value_len); + local res = coraza.coraza_add_response_header(transaction, cast_to_c_char(header_name), #header_name, + cast_to_c_char(header_value), #header_value) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_add_response_header with %s:%s", + ngx_ctx.request_id, header_name, header_value)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_add_response_header with %s:%s", + ngx_ctx.request_id, header_name, header_value)) + end +end + +function _M.append_response_body(transaction, body) + local res = coraza.coraza_append_response_body(transaction, cast_to_c_char(body), #body) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_append_response_body with %s", + ngx_ctx.request_id, body)) + else + nlog(debug_fmt("Transaction %s success to invoke coraza_append_response_body with %s", + ngx_ctx.request_id, body)) + end +end + +function _M.process_response_body(transaction) + local res = coraza.coraza_process_response_body(transaction) + if res == 1 then + nlog(err_fmt("Transaction %s failed to invoke coraza_process_response_body", + ngx_ctx.request_id)) + else + nlog(debug_fmt("Transaction %s uccess to invoke coraza_process_response_body", + ngx_ctx.request_id)) + end +end + +return _M \ No newline at end of file diff --git a/lib/resty/coraza/log.lua b/lib/resty/coraza/log.lua new file mode 100644 index 0000000..b4f233e --- /dev/null +++ b/lib/resty/coraza/log.lua @@ -0,0 +1,33 @@ +--- +--- Generated by EmmyLua(https://github.com/EmmyLua) +--- Created by mac. +--- DateTime: 2023/6/14 09:47 +--- +--- +local _M = { + _VERSION = '1.0.0' +} + +local fmt = string.format + +local ERR = ngx.ERR +local WARN = ngx.WARN +local DEBUG = ngx.DEBUG + +local function log(formatstring, ...) + return fmt(ngx.get_phase().." phrase ".."lua-resty-coraza: "..formatstring, ...) +end + +function _M.err_fmt(formatstring, ...) + return ERR, log(formatstring, ...) +end + +function _M.warn_fmt(formatstring, ...) + return WARN, log(formatstring, ...) +end + +function _M.debug_fmt(formatstring, ...) + return DEBUG, log(formatstring, ...) +end + +return _M \ No newline at end of file diff --git a/lib/resty/coraza/request.lua b/lib/resty/coraza/request.lua new file mode 100644 index 0000000..9ea1452 --- /dev/null +++ b/lib/resty/coraza/request.lua @@ -0,0 +1,55 @@ +local coraza = require "resty.coraza.coraza" + +local fmt = string.format + +local ngx = ngx +local nlog = ngx.log +local ngx_req = ngx.req + +local _M = { + _VERSION = '1.0.0', +} + + +function _M.build_and_process_header(transaction) + local headers, err = ngx_req.get_headers(0, true) + if err then + err = fmt("failed to call ngx_req.get_headers: %s", err) + nlog(ngx.ERR, err) + end + for k, v in pairs(headers) do + coraza.add_request_header(transaction, k, v) + end + coraza.process_request_headers(transaction) +end + +function _M.build_and_process_body(transaction) + local req_body = ngx_req.get_body_data() + if not req_body then + -- TODO: fix code + local path = ngx_req.get_body_file() + if not path then + -- end process + return + end + coraza.request_body_from_file(path) + else + local req_body_size = #req_body + -- TODO req_body_size > req_body_size_opt + coraza.append_request_body(transaction, req_body) + end + coraza.process_request_body(transaction) +end + +function _M.build_and_process_get_args(transaction) + -- process http get args if has + local arg = ngx_req.get_uri_args() + for k,v in pairs(arg) do + coraza.add_get_args(transaction, k, v) + end +end + +return _M + + + diff --git a/t/coraza.conf b/t/coraza.conf new file mode 100644 index 0000000..be3ec95 --- /dev/null +++ b/t/coraza.conf @@ -0,0 +1,249 @@ +# -- Rule engine initialization ---------------------------------------------- + +# Enable Coraza, attaching it to every transaction. Use detection +# only to start with, because that minimises the chances of post-installation +# disruption. +# +SecRuleEngine On +#SecRuleEngine DetectionOnly + + +# -- Request body handling --------------------------------------------------- + +# Allow Coraza to access request bodies. If you don't, Coraza +# won't be able to see any POST parameters, which opens a large security +# hole for attackers to exploit. +# +SecRequestBodyAccess On + +# Enable XML request body parser. +# Initiate XML Processor in case of xml content-type +# +SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \ + "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + +# Enable JSON request body parser. +# Initiate JSON Processor in case of JSON content-type; change accordingly +# if your application does not use 'application/json' +# +SecRule REQUEST_HEADERS:Content-Type "^application/json" \ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + +# Sample rule to enable JSON request body parser for more subtypes. +# Uncomment or adapt this rule if you want to engage the JSON +# Processor for "+json" subtypes +# +#SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \ +# "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + +# Maximum request body size we will accept for buffering. If you support +# file uploads then the value given on the first line has to be as large +# as the largest file you are willing to accept. The second value refers +# to the size of data, with files excluded. You want to keep that value as +# low as practical. +# +SecRequestBodyLimit 13107200 + +SecRequestBodyInMemoryLimit 131072 + +SecRequestBodyNoFilesLimit 131072 + +# What to do if the request body size is above our configured limit. +# Keep in mind that this setting will automatically be set to ProcessPartial +# when SecRuleEngine is set to DetectionOnly mode in order to minimize +# disruptions when initially deploying Coraza. +# +SecRequestBodyLimitAction Reject + +# Verify that we've correctly processed the request body. +# As a rule of thumb, when failing to process a request body +# you should reject the request (when deployed in blocking mode) +# or log a high-severity alert (when deployed in detection-only mode). +# +SecRule REQBODY_ERROR "!@eq 0" \ +"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + +# By default be strict with what we accept in the multipart/form-data +# request body. If the rule below proves to be too strict for your +# environment consider changing it to detection-only. You are encouraged +# _not_ to remove it altogether. +# +SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ +"id:'200003',phase:2,t:none,log,deny,status:400, \ +msg:'Multipart request body failed strict validation: \ +PE %{REQBODY_PROCESSOR_ERROR}, \ +BQ %{MULTIPART_BOUNDARY_QUOTED}, \ +BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ +DB %{MULTIPART_DATA_BEFORE}, \ +DA %{MULTIPART_DATA_AFTER}, \ +HF %{MULTIPART_HEADER_FOLDING}, \ +LF %{MULTIPART_LF_LINE}, \ +SM %{MULTIPART_MISSING_SEMICOLON}, \ +IQ %{MULTIPART_INVALID_QUOTING}, \ +IP %{MULTIPART_INVALID_PART}, \ +IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ +FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + +# Did we see anything that might be a boundary? +# +# Here is a short description about the Coraza Multipart parser: the +# parser returns with value 0, if all "boundary-like" line matches with +# the boundary string which given in MIME header. In any other cases it returns +# with different value, eg. 1 or 2. +# +# The RFC 1341 descript the multipart content-type and its syntax must contains +# only three mandatory lines (above the content): +# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING +# * --BOUNDARY_STRING +# * --BOUNDARY_STRING-- +# +# First line indicates, that this is a multipart content, second shows that +# here starts a part of the multipart content, third shows the end of content. +# +# If there are any other lines, which starts with "--", then it should be +# another boundary id - or not. +# +# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive. +# +# If multipart content contains the three necessary lines with correct order, but +# there are one or more lines with "--", then parser returns with value 2 (non-zero). +# +# If some of the necessary lines (usually the start or end) misses, or the order +# is wrong, then parser returns with value 1 (also a non-zero). +# +# You can choose, which one is what you need. The example below contains the +# 'strict' mode, which means if there are any lines with start of "--", then +# Coraza blocked the content. But the next, commented example contains +# the 'permissive' mode, then you check only if the necessary lines exists in +# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."), +# or other text files, which contains eg. HTTP headers. +# +# The difference is only the operator - in strict mode (first) the content blocked +# in case of any non-zero value. In permissive mode (second, commented) the +# content blocked only if the value is explicit 1. If it 0 or 2, the content will +# allowed. +# + +# +# See #1747 and #1924 for further information on the possible values for +# MULTIPART_UNMATCHED_BOUNDARY. +# +SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \ + "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" + +# Some internal errors will set flags in TX and we will need to look for these. +# All of these are prefixed with "MSC_". The following flags currently exist: +# +# COR_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. +# +SecRule TX:/^COR_/ "!@streq 0" \ + "id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'" + + +# -- Response body handling -------------------------------------------------- + +# Allow Coraza to access response bodies. +# You should have this directive enabled in order to identify errors +# and data leakage issues. +# +# Do keep in mind that enabling this directive does increases both +# memory consumption and response latency. +# +SecResponseBodyAccess On + +# Which response MIME types do you want to inspect? You should adjust the +# configuration below to catch documents but avoid static files +# (e.g., images and archives). +# +SecResponseBodyMimeType text/plain text/html text/xml + +# Buffer response bodies of up to 512 KB in length. +SecResponseBodyLimit 524288 + +# What happens when we encounter a response body larger than the configured +# limit? By default, we process what we have and let the rest through. +# That's somewhat less secure, but does not break any legitimate pages. +# +SecResponseBodyLimitAction ProcessPartial + + +# -- Filesystem configuration ------------------------------------------------ + +# The location where Coraza will keep its persistent data. This default setting +# is chosen due to all systems have /tmp available however, it +# too should be updated to a place that other users can't access. +# +SecDataDir /tmp/ + + +# -- File uploads handling configuration ------------------------------------- + +# The location where Coraza stores intercepted uploaded files. This +# location must be private to Coraza. You don't want other users on +# the server to access the files, do you? +# +#SecUploadDir /opt/coraza/var/upload/ + +# By default, only keep the files that were determined to be unusual +# in some way (by an external inspection script). For this to work you +# will also need at least one file inspection rule. +# +#SecUploadKeepFiles RelevantOnly + +# Uploaded files are by default created with permissions that do not allow +# any other user to access them. You may need to relax that if you want to +# interface Coraza to an external program (e.g., an anti-virus). +# +#SecUploadFileMode 0600 + + +# -- Debug log configuration ------------------------------------------------- + +# Default debug log path +# Debug levels: +# 0: No logging (least verbose) +# 1: Error +# 2: Warn +# 3: Info +# 4-8: Debug +# 9: Trace (most verbose) +# Most logging has not been implemented because it will be replaced with +# advanced rule profiling options +SecDebugLog debug.log +SecDebugLogLevel 1 + + +# -- Audit log configuration ------------------------------------------------- + +# Log the transactions that are marked by a rule, as well as those that +# trigger a server error (determined by a 5xx or 4xx, excluding 404, +# level response status codes). +# +#SecAuditEngine RelevantOnly +SecAuditEngine On +SecAuditLog audit.log +#SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$" +SecAuditLogFormat JSON + +# Log everything we know about a transaction. +SecAuditLogParts ABIJDEFHKZ + +# Use a single file for logging. This is much easier to look at, but +# assumes that you will use the audit log only occasionally. +# +SecAuditLogType Serial + + +# -- Miscellaneous ----------------------------------------------------------- + +# Use the most commonly used application/x-www-form-urlencoded parameter +# separator. There's probably only one application somewhere that uses +# something else so don't expect to change this value. +# +SecArgumentSeparator & + +# Settle on version 0 (zero) cookies, as that is what most applications +# use. Using an incorrect cookie version may open your installation to +# evasion attacks (against the rules that examine named cookies). +# +SecCookieFormat 0 diff --git a/t/default.conf b/t/default.conf new file mode 100644 index 0000000..9bd1611 --- /dev/null +++ b/t/default.conf @@ -0,0 +1,4 @@ +SecDebugLogLevel 9 +SecDebugLog /dev/stdout +SecAuditEngine On +#SecRule REQUEST_HEADERS:User-Agent "Mozilla" "phase:1, id:3,drop,status:403,log,msg:'Blocked User-Agent'" diff --git a/t/integration_block_req_header.t b/t/integration_block_req_header.t new file mode 100644 index 0000000..74a0b5d --- /dev/null +++ b/t/integration_block_req_header.t @@ -0,0 +1,54 @@ +use Test::Nginx::Socket 'no_plan'; + +our $HttpConfig = <<'_EOC_'; + lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; + lua_socket_log_errors off; + lua_code_cache on; + lua_need_request_body on; + init_worker_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_init() + coraza.rules_add([[SecRule REQUEST_HEADERS:User-Agent "Mozilla" "phase:1, id:3,drop,status:452,log,msg:'Blocked User-Agent'"]]) + } +_EOC_ + +our $LocationConfig = <<'_EOC_'; + location /t { + access_by_lua_block { + local coraza = require "resty.coraza" + coraza.do_access_filter() + } + + content_by_lua_block { + ngx.say("passed") + } + + header_filter_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_header_filter() + } + + log_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_free() + } + } +_EOC_ + +# master_on(); +# workers(4); +run_tests(); + +__DATA__ + +=== TEST 1: integration test blocked +--- http_config eval: $::HttpConfig +--- config eval: $::LocationConfig +--- more_headers +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.44 +--- request +POST /t/shell.php +aaaaaaaaa=aaaaaa +--- error_code: 452 +--- response_body_like eval +'{"code": 452, "message": "This connection was blocked by Coroza!"}' \ No newline at end of file diff --git a/t/integration_block_resp_header.t b/t/integration_block_resp_header.t new file mode 100644 index 0000000..3b8069e --- /dev/null +++ b/t/integration_block_resp_header.t @@ -0,0 +1,52 @@ +use Test::Nginx::Socket 'no_plan'; + +our $HttpConfig = <<'_EOC_'; + lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; + lua_socket_log_errors off; + lua_code_cache on; + lua_need_request_body on; + init_worker_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_init() + coraza.rules_add([[SecRule RESPONSE_HEADERS:Content-Type "text" "phase:3, id:4,drop,status:451,log,msg:'Blocked content-type'"]]) + } +_EOC_ + +our $LocationConfig = <<'_EOC_'; + location /t { + access_by_lua_block { + local coraza = require "resty.coraza" + coraza.do_access_filter() + } + + content_by_lua_block { + ngx.say("passed") + } + + header_filter_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_header_filter() + } + + log_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_free() + } + } +_EOC_ + +# master_on(); +# workers(4); +run_tests(); + +__DATA__ + +=== TEST 1: integration test blocked +--- http_config eval: $::HttpConfig +--- config eval: $::LocationConfig +--- request +POST /t/shell.php?a=b +aaaaaaaaa=aaaaaa +--- error_code: 451 +--- response_body_like eval +"" \ No newline at end of file diff --git a/t/integration_passed.t b/t/integration_passed.t new file mode 100644 index 0000000..4229206 --- /dev/null +++ b/t/integration_passed.t @@ -0,0 +1,51 @@ +use Test::Nginx::Socket 'no_plan'; + +our $HttpConfig = <<'_EOC_'; + lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; + lua_socket_log_errors off; + lua_code_cache on; + lua_need_request_body on; + init_worker_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_init() + } +_EOC_ + +our $LocationConfig = <<'_EOC_'; + location /t { + access_by_lua_block { + local coraza = require "resty.coraza" + coraza.do_access_filter() + } + + content_by_lua_block { + ngx.say("passed") + } + + header_filter_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_header_filter() + } + + log_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_free() + } + } +_EOC_ + +# master_on(); +# workers(4); +run_tests(); + +__DATA__ + +=== TEST 1: integration test blocked +--- http_config eval: $::HttpConfig +--- config eval: $::LocationConfig +--- request +POST /t/shell.php +aaaaaaaaa=aaaaaa +--- error_code: 200 +--- response_body_like eval +"passed" \ No newline at end of file diff --git a/t/integration_with_coreruleset.t b/t/integration_with_coreruleset.t new file mode 100644 index 0000000..8241c47 --- /dev/null +++ b/t/integration_with_coreruleset.t @@ -0,0 +1,53 @@ +use Test::Nginx::Socket 'no_plan'; + +our $HttpConfig = <<'_EOC_'; + lua_package_path "lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; + lua_socket_log_errors off; + lua_code_cache on; + lua_need_request_body on; + init_worker_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_init() + coraza.rules_add_file("/Users/mac/GolandProjects/libcoraza/lua-resty-coroza/lib/resty/coraza.conf") + coraza.rules_add("Include /Users/mac/Downloads/coreruleset-4.0-dev/rules/*.conf") + } +_EOC_ + +our $LocationConfig = <<'_EOC_'; + location /t { + access_by_lua_block { + local coraza = require "resty.coraza" + coraza.do_access_filter() + } + + content_by_lua_block { + ngx.say("passed") + } + + header_filter_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_header_filter() + } + + log_by_lua_block{ + local coraza = require "resty.coraza" + coraza.do_free() + } + } +_EOC_ + +# master_on(); +# workers(4); +run_tests(); + +__DATA__ + +=== TEST 1: integration test blocked +--- http_config eval: $::HttpConfig +--- config eval: $::LocationConfig +--- request +POST /t/shell.php +aaaaaaaaa=aaaaaa +--- error_code: 200 +--- response_body_like eval +"passed"