I'm trying to create a ModSecurity rule (I'm actually using Coraza, but it should be the same thing) to reject a request if it contains a JSON value in a POST request body. Here's what I have:
SecRule REQUEST_BODY "@contains \"foo\":\"bar\"" "id:1001,phase:2,deny,log"
The following request is not rejected:
curl -v -H "Content-Type: application/json" -X POST -d '{"foo":"bar"}' http://localhost
Even the following rule doesn't work:
SecRule REQUEST_BODY "@contains foo" "id:1001,phase:2,deny,log"
I see this in the log:
2023/10/31 17:28:18 [DEBUG] Evaluating operator: NO MATCH tx_id="WtmgMAOcaYrcfgawZRd" rule_id=1001 operator_function="@contains" operator_data="foo" arg=""
OWASP ModSecurity Core Rule Set Dev on Duty and Coraza Maintainer here.
Assuming that you are running Coranza with the coraza.conf-recommended configuration, a fundamental rule that will have an impact on your outcome is the 200001
:
SecRule REQUEST_HEADERS:Content-Type "^application/json" \ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
Being application/json
the content type of the request, this rule will be triggered and the body processor that is going to handle your request will be set to the JSON one. Citing both Modsec and Coraza docs regarding the REQUEST_BODY
variable:
Holds the raw request body. This variable is available only if the URLENCODED request body processor was used, which will occur by default when the application/x-www-form-urlencoded content type is detected, or if the use of the URLENCODED request body parser was forced.
Therefore, in that case the REQUEST_BODY
variable is not going to be populated. What happens is that the body request is processed by the specific body processor that reads the JSON content, and splits it into args (specifically args_post).
An example rule that would match {"foo":"bar"}
sent by the request
curl -v -H "Content-Type: application/json" -X POST -d '{"foo":"bar"}' http://localhost
is the following:
SecRule ARGS:json.foo "@contains bar" "id:1001,phase:2,deny,log"
Here we are looking for a specific json ARG (even ARGS_POST would work) with foo
as key and the value has to contain bar
.
That being said, besides the previously quoted doc reference, I agree that it is not intuitive to figure out how the different body processor works. I'm taking notes to give some care to the Coraza doc about this topic!
Edit:
This branch provides a small example based on caddy-coraza.
Steps to reproduce:
debug
log level:mage buildCaddyLinux && mage runExample && docker-compose -f ./example/docker-compose.yml logs -f caddy-logs
▶ curl -v -H "Content-Type: application/json" -d '{"foo":"bar"}' http://localhost:8080/anything
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /anything HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.1.2
> Accept: */*
> Content-Type: application/json
> Content-Length: 13
>
< HTTP/1.1 403 Forbidden
< Server: Caddy
< Date: Thu, 02 Nov 2023 21:53:53 GMT
< Content-Length: 0
<
Most relevant logs show:
Evaluating operator: MATCH","tx_id":"JCGZVWrlOcIeTYgz","rule_id":200001,"operator_function":"@rx","operator_data":"^application/json","arg":"application/json"
Evaluating rule","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001
Expanding arguments for rule","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001,"variable":"ARGS"
Transforming argument for rule","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001
Arguments transformed for rule","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001
Matching rule","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001,"variable_name":"ARGS","key":"json.foo"
Evaluating operator: MATCH","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001,"operator_function":"@contains","operator_data":"bar","arg":"bar"
Executing disruptive action for rule","tx_id":"JCGZVWrlOcIeTYgz","rule_id":1001,"action":"deny"
Coraza: Access denied (phase 2). [file ""] [line "251"] [id "1001"] [rev ""] [msg ""] [data ""] [severity "emergency"] [ver ""] [maturity "0"] [accuracy "0"] [hostname ""] [uri "/anything"] [unique_id "JCGZVWrlOcIeTYgz"]
Alternatively:
The main repo of Coraza comes with an http-server example. It is possible to tweak the default.conf file as below:
SecDebugLogLevel 9
SecDebugLog /dev/stdout
SecRuleEngine On
SecRequestBodyAccess On
SecRule REQUEST_HEADERS:Content-Type "^application/json" "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
SecRule ARGS:json.foo "@contains bar" "id:1001,phase:2,deny,log"
Curl request:
▶ curl -v -H "Content-Type: application/json" -d '{"foo":"bar"}' http://localhost:8090
* Trying 127.0.0.1:8090...
* Connected to localhost (127.0.0.1) port 8090 (#0)
> POST / HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/8.1.2
> Accept: */*
> Content-Type: application/json
> Content-Length: 13
>
< HTTP/1.1 403 Forbidden
< Date: Thu, 02 Nov 2023 22:05:05 GMT
< Content-Length: 0
<