mod-security

ModSecurity rule to find JSON value in request body


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=""

Solution

  • 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:

    Example with caddy-coraza

    This branch provides a small example based on caddy-coraza.

    Steps to reproduce:

    1. Build the module, run the example and print logs at debug log level:
    mage buildCaddyLinux && mage runExample && docker-compose -f ./example/docker-compose.yml logs -f caddy-logs
    
    1. Perform the curl request:
    ▶ 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:

    Example with Go http-server

    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
    <