I'm setting up a (self-hosted) ghost blog, running behind nginx. I've recently added the ModSecurity v3 (aka libModSecurity) WAF with the OWASP CRS v3 ruleset, but I'm having trouble ironing out some false positives.
My problem is very similar to this question, but the suggested solution there doesn't work for me.
The rule that's getting triggered is:
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"
I think the cause of the problem is that ghost (rightly or wrongly) is specifying a Content-Type: application/json
header for an ajax GET
request, to download some config data as Json.
The request is blocked because this Content-Type header triggers the JSON parser for the request body:
SecRule REQUEST_HEADERS:Content-Type "application/json" \
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
But, this is a GET request without a request body, so the JSON parser complains that JSON parsing error: parse error: premature EOF
... i.e. there's no Json to parse.
After attempting to read umpteen guides, the ModSecurity documentation and many helpful answers on SO from Barry Pollard (e.g. here, here and here), I have tried to write a 'whitelist' rule that's specific to my trigger scenario (rather than disabling the triggered rule globally).
Thus, I tried using the following chained rule to exclude the request body from parsing: only for GET
requests, to URIs starting with /ghost/
and when they have application/json
specified for Content-Type...
SecRule REQUEST_URI "^/ghost/" "phase:1,id:1200,t:none,t:urlDecode,t:lowercase,t:normalizePath,pass,ctl:requestBodyAccess=Off,chain"
SecRule REQUEST_METHOD "@streq get" "t:none,t:lowercase,chain"
SecRule REQUEST_HEADERS:Content-Type "application/json" "t:none,t:lowercase"
To my disappointment, this doesn't work - my rule is in the audit log right before the rule 200002 is also logged. In other words, ctl:requestBodyAccess=Off
doesn't seem to prevent the JSON parser from getting triggered. Here's the relevant bit of the audit log for a failed transaction:
ModSecurity: Warning. Matched "Operator `Rx' with parameter `application/json' against variable `REQUEST_HEADERS:content-type' (Value: `application/json; charset=UTF-8' ) [file "/etc/nginx/modsec/modsecurity.conf"] [line "8"] [id "1200"] [rev ""] [msg ""] [data ""] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "xxx.xxx.xxx.xxx"] [uri "/ghost/api/v0.1/configuration/"] [unique_id "152042340176.931426"] [ref "o0,7v4,30t:urlDecode,t:lowercase,t:normalizePathv0,3t:lowercaseo0,16v416,31t:lowercase"]
ModSecurity: Access denied with code 400 (phase 2). Matched "Operator `Eq' with parameter `0' against variable `REQBODY_ERROR' (Value: `1' ) [file "/etc/nginx/modsec/modsecurity.conf"] [line "51"] [id "200002"] [rev ""] [msg "Failed to parse request body."] [data "JSON parsing error: parse error: premature EOF\x0a"] [severity "2"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "xxx.xxx.xxx.xxx"] [uri "/ghost/api/v0.1/configuration/"] [unique_id "152042340176.931426"] [ref "v561,1"]
FWIW, I also tried the simpler rule suggested by Barry for the other, similar, question:
SecRule REQUEST_BODY_LENGTH "@eq 0" "phase:1,id:'12345',ctl:requestBodyAccess=Off"
But that doesn't make a difference either. Being a GET
, there's no Content-Length
specified in the request headers, so maybe that's why it's failing?