How It Works
OAShield turns an OpenAPI specification into a positive security model: a set of WAF rules that permit exactly the requests your API declares and deny everything else. This page explains what the generated rules actually do.
The big picture
Your OpenAPI spec already describes every legitimate endpoint, method, parameter, and body field. OAShield compiles that description into SecLang rules that:
- Match the incoming request against each declared operation.
- Validate the request’s parameters and body against that operation’s schema.
- Deny by default if no operation matched, or if validation failed.
The result runs on any engine that speaks SecLang, meaning OWASP ModSecurity v3 or Coraza.
Anatomy of the generated rules
OAShield emits one .conf file per API tag (e.g. PetApi.conf, StoreApi.conf). Every
file is a sequence of operation blocks followed by a single default-deny at the
end. The examples below come from the
Petstore sample output.
1. Operation matching
Each block begins by asking “is this request for this operation?” Three checks gate the
block: path, exact-path (no extra segments), and method. If any check fails, skipAfter
jumps past the block, because this request isn’t for this operation.
# addPet: POST /petSecRule REQUEST_URI "!@restpath /pet" "id:4200001,phase:2,pass,nolog,skipAfter:END_addPet"SecRule REQUEST_URI "!@rx ^/pet(\?.*)?$" "id:4200002,phase:2,pass,nolog,skipAfter:END_addPet"SecRule REQUEST_METHOD "!@within POST" "id:4200003,phase:2,pass,nolog,skipAfter:END_addPet"@restpath matches the OpenAPI path template (including {petId}-style placeholders).
The @rx line rejects requests with extra trailing segments, so /pet/extra can’t slip
into the /pet block.
2. Parameter validation
Once a request is confirmed to belong to an operation, its parameters are checked against
the schema. Values that don’t match are denied outright; a skipAfter:FAILED_API_CHECKS
sends them to the default-deny at the end of the file:
# path parameter petId, typed as integerSecRule ARGS_PATH:petId "!@rx ^[0-9]{1,19}$" "id:4210021,phase:2,deny,status:403,msg:'Forbidden parameter value detected',...,skipAfter:FAILED_API_CHECKS"SecRule &ARGS_PATH:petId "@gt 1" "id:4210022,phase:2,deny,status:403,msg:'Multiple values for non-array parameter',...,skipAfter:FAILED_API_CHECKS"
# enum query parameter statusSecRule ARGS_GET:status "!@rx ^(available|pending|sold)$" "id:4210063,phase:2,deny,status:403,...,skipAfter:FAILED_API_CHECKS"Each parameter contributes two kinds of rule: a value check (@rx derived from the
schema’s type, enum, pattern, or numeric bounds) and a cardinality check (&ARGS…
counts occurrences, so a non-array parameter can only appear once).
3. Unknown-parameter allowlist
After the declared parameters are validated, anything not declared is rejected. The allowlist regex names the permitted parameters, and a request carrying any other parameter is blocked:
# operation with no query params: any query arg is unknownSecRule ARGS_GET_NAMES "@rx ^.+$" "id:4200007,phase:2,block,msg:'Unknown parameter detected',...,skipAfter:FAILED_API_CHECKS"
# operation that declares `status`: anything else is unknownSecRule ARGS_GET_NAMES "!@rx ^(status)$" "id:4200046,phase:2,block,msg:'Unknown parameter detected',...,skipAfter:FAILED_API_CHECKS"4. Operation passed: allow and exit
If the request survived every check, it’s a valid call to this operation. The block allows it and closes with its end marker:
SecAction "phase:2,allow:request,id:4200008"SecMarker END_addPet5. Default deny
At the very end of the file sits the target of every FAILED_API_CHECKS jump, and the
fall-through for any request that matched no operation at all:
# For anything else, deny by defaultSecMarker FAILED_API_CHECKSSecAction "id:4220001,log,auditlog,block,phase:2,msg:'Unknown API endpoint'"This is what makes the model positive: reaching the end of the file without an explicit
allow means the request is denied.
Request-body validation
Parameters cover the query string, path, and form fields. JSON request bodies are handled separately, and this is where the two engine flavors differ:
-
modsecurity3(default): per-field rules generated from the schema, covering required-property presence, per-property type patterns, numericminimum/maximum, and anARGS_NAMESallowlist that rejects undeclared properties. This works on both engines because it uses only standard operators. -
coraza: everything above, plus a@validateSchemarule that validates the raw body against a generated JSON Schema file. That covers constraints per-field rules can’t express: exact type distinctions, per-array-elementrequired, deep nesting,oneOfsemantics, and more.
The per-field approach runs after the engine flattens JSON into string parameters, so
it has some inherent limits. Those are listed in
Configuration → Limitations,
and they’re exactly the cases Coraza’s @validateSchema covers in full.
Why per-tag files and phase 2?
- Phase 2 is the request-body phase. By then the URI, method, query args, and body are all available, so a single pass can validate the whole request.
- One file per tag keeps the rules readable and lets you deploy or review subsets of your API independently. Load them all together and the final default-deny in each still applies to its own operations.
See it yourself
The fastest way to build intuition is to generate rules for a spec you know and read the output:
java -cp oashield-cli.jar org.openapitools.codegen.OpenAPIGenerator \ generate -g modsecurity3 -i samples/petstore.yaml -o output/Then open output/PetApi.conf and follow the blocks top to bottom. The
Petstore sample output
is already checked in if you’d rather just browse.