Skip to content

v0.3.0 — JSON request body inspection

Choose a tag to compare

@raffaelschneider raffaelschneider released this 22 Aug 15:31
· 13 commits to main since this release
81bbf5c

JSON request bodies are now inspected. Before this release they were not.

The headline

A JSON body fell through to the urlencoded parser, which extracts nothing usable from it. ARGS was empty for every JSON request, so CRS 942100 — SecRule ARGS "@detectSQLi", the core SQLi rule — had nothing to look at. The identical payload was blocked in a form body and passed unexamined in a JSON body:

urlencoded body, SQLi in q      -> blocked=true
JSON body, SQLi in q            -> blocked=false     (0.2.0)
JSON body, SQLi in q            -> blocked=true      (0.3.0)

For a JSON API that was most of the attack surface the engine was deployed to cover.

Scalars are flattened into ARGS named by path — json.user.name, json.tags.0 — matching libmodsecurity, so CRS exclusions written against those names work unchanged. Dispatch covers application/json, text/json and the RFC 6839 +json suffix, plus ctl:requestBodyProcessor=JSON.

Two evasions were closed along the way. Duplicate object keys are all inspected rather than only the last, so {"a":"<payload>","a":"safe"} no longer hides a payload from the engine while an origin that keeps the first value receives it. And REQBODY_ERROR now resolves, so CRS rule 200002 can act on a body that failed to parse — a strict parser rejecting what the application accepts is itself a bypass when the rejection is silent.

ctl: actions execute

ctl: parsed cleanly and was then discarded. Now implemented per transaction: ruleEngine, ruleRemoveById (single ID, range, or list), ruleRemoveTargetById, requestBodyAccess, requestBodyProcessor.

State lives on the transaction, never on the shared ruleset — mutating a ruleset behind an Arc would let one request disable a rule for every request in flight.

Unusable regex patterns are reported

A pattern was compiled lazily and failure was swallowed into a no-match, so a typo produced a rule that could never match, for every request, with nothing logged. Syntax is now validated at load and an unusable pattern is reported with its rule ID. The rule is kept rather than failing the load: one pattern this engine cannot parse should not stop a whole ruleset from loading.

Upgrading

Re-test your ruleset against representative traffic. This changes which requests are blocked, in both directions:

  • ctl:ruleRemoveTargetById now filters resolved values rather than variable specifications, so an exclusion like 942100;ARGS:json.token against a rule targeting ARGS finally applies. Rules that fired despite an exclusion will correctly stop firing.
  • Rules with an unusable @rx pattern were already never matching; they are now visible in the log. None change from not-matching to matching.
  • JSON bodies are inspected where they previously were not, so genuine detections will appear that did not before — along with any false positives your ruleset has for JSON traffic.

Still not implemented

XML request bodies. ctl:requestBodyProcessor=XML reports itself as unsupported at load rather than being silently accepted, so the gap is stated rather than hidden — but XML bodies go uninspected.

Tracked with the remaining gaps in zentinelproxy/zentinel#340.