Custom Rules Reference¶
Auto-generated reference for the JSON Schema that the SaaS frontend and the scanner runtime accept. Both surfaces are covered below.
Shared models (AlertModel, Compliance, CustomRuleID, ...) are documented once in the API reference; the WebApp reference lists only WebApp-specific models and links back to the API section for shared definitions.
API reference¶
APICustomRule¶
Objects¶
APIExtractor¶
APILogicalAndDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
and* | List[APILogicalAndDetector,APILogicalNotDetector,APILogicalOrDetector,FingerprintCountDetector,FingerprintsSameDetector,HelpersRequestCrudDetector,HelpersResponseIsSuccessfulDetector,JSONMatchesAllDetector,JSONMatchesCountDetector,RegexMatchesAllDetector,RegexMatchesCountDetector,RequestBodyJSONDetector,RequestBodyTextDetector,RequestHeadersDetector,RequestIsAuthenticatedDetector,RequestMethodDetector,RequestObjectDetector,RequestUserDetector,ResponseBodyJSONDetector,ResponseBodyTextDetector,ResponseDurationDetector,ResponseHeadersDetector,ResponseObjectDetector,ResponseStatusCodeDetector,ScanTypeDetector,SchemaNeedAuthenticationDetector,SchemaPathRefDetector,SchemaUrlDetector,VariableDefinedDetector] | Logical and on a list of detectors | |
if* | Const[and] | and | Use this to apply a logical and on a list of detectors.### Exampleyamldetect: - if: and and: - if: helpers.request.crud in: - CREATE - UPDATE - if: response.status_code is: 200 |
APILogicalNotDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[not] | not | Use this to apply a logical not on a detector.### Exampleyamldetect: - if: not not: if: response.status_code is: 200 |
not | APILogicalAndDetector, APILogicalNotDetector, APILogicalOrDetector, FingerprintCountDetector, FingerprintsSameDetector, HelpersRequestCrudDetector, HelpersResponseIsSuccessfulDetector, JSONMatchesAllDetector, JSONMatchesCountDetector, RegexMatchesAllDetector, RegexMatchesCountDetector, RequestBodyJSONDetector, RequestBodyTextDetector, RequestHeadersDetector, RequestIsAuthenticatedDetector, RequestMethodDetector, RequestObjectDetector, RequestUserDetector, ResponseBodyJSONDetector, ResponseBodyTextDetector, ResponseDurationDetector, ResponseHeadersDetector, ResponseObjectDetector, ResponseStatusCodeDetector, ScanTypeDetector, SchemaNeedAuthenticationDetector, SchemaPathRefDetector, SchemaUrlDetector, VariableDefinedDetector | null | Logical not of a detector |
APILogicalOrDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[or] | or | Use this to apply a logical or on a list of detectors.### Exampleyamldetect: - if: or or: - if: helpers.request.crud in: - CREATE - UPDATE - if: response.status_code is: 200 |
or* | List[APILogicalAndDetector,APILogicalNotDetector,APILogicalOrDetector,FingerprintCountDetector,FingerprintsSameDetector,HelpersRequestCrudDetector,HelpersResponseIsSuccessfulDetector,JSONMatchesAllDetector,JSONMatchesCountDetector,RegexMatchesAllDetector,RegexMatchesCountDetector,RequestBodyJSONDetector,RequestBodyTextDetector,RequestHeadersDetector,RequestIsAuthenticatedDetector,RequestMethodDetector,RequestObjectDetector,RequestUserDetector,ResponseBodyJSONDetector,ResponseBodyTextDetector,ResponseDurationDetector,ResponseHeadersDetector,ResponseObjectDetector,ResponseStatusCodeDetector,ScanTypeDetector,SchemaNeedAuthenticationDetector,SchemaPathRefDetector,SchemaUrlDetector,VariableDefinedDetector] | Logical or on a list of detectors |
AlertModel¶
| Property | Type | Default | Description |
|---|---|---|---|
category | CustomRuleCategory | CUSTOM | Category of the alert |
compliance | Compliance | null | Compliance standards violated by this alert |
context* | string | Context of the alert | |
description | string | null | Description of the alert |
name* | string | Name of the alert | |
remediation | string | null | Remediation of the alert |
severity* | CustomRuleSeverity | Severity of the alert |
BaseStringMutate¶
| Property | Type | Default | Description |
|---|---|---|---|
regex_replace | RegexReplace | null | Regex replace pattern. |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
value | string | null | The value to set. |
values | List[string] | null | The values to set, generates multiple queries. |
Compliance¶
| Property | Type | Default | Description |
|---|---|---|---|
cra | string | null | |
cwe | string | null | |
dora | string | null | |
fedramp | string | null | |
gdpr | string | null | |
hipaa | string | null | |
hitrust_csf | string | null | |
iec62443 | string | null | |
iso27001 | string | null | |
mitre_attack | string | null | |
nis2 | string | null | |
nist | string | null | |
owasp | string | null | |
owasp_asvs | string | null | |
owasp_llm | string | null | |
pci-dss | string | null | |
psd2 | string | null | |
soc2 | string | null | |
wasc | string | null |
CurlSeeder¶
| Property | Type | Default | Description |
|---|---|---|---|
curl* | string | The curl command to use for the request. | |
protocol* | Const[curl] | curl | Inject a request specified as a curl command. Useful when youalready have a working curl one-liner from browser DevTools(Network tab → Right-click → Copy as cURL) or Postman, and youwant to drop it in as-is.yamlseed: - protocol: curl user: admin curl: > curl -X POST '[example.com](https://example.com/api/users') -H 'Content-Type: application/json' -d '{"name": "tester"}'For long-lived rules prefer the rest seeder (clearer diff,auto-filled host). |
user | string | null | The user to use for the request. If not provided, the request is sent without authentication. |
CustomRuleID¶
| Property | Type | Default | Description |
|---|---|---|---|
FingerprintCountDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
gt | integer | null | Condition is greater than this integer |
if* | Const[helpers.fingerprints.count] | helpers.fingerprints.count | Use this to select and compare the count of unique fingerprints of the current and original response. |
in | List[integer] | null | Condition is in this list of integers (exact match) |
is | integer | null | Condition is this exact integer |
is_not | integer | null | Condition is not this exact integer |
lt | integer | null | Condition is less than this integer |
FingerprintsSameDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[helpers.fingerprints.same] | helpers.fingerprints.same | Use this to determine whether the current and original responses have the same fingerprint. |
is | boolean | null | Condition is true |
is_not | boolean | null | Condition is false |
HTTPRAWSeeder¶
| Property | Type | Default | Description |
|---|---|---|---|
protocol* | Const[http] | http | Inject a request at scan start, formatted as a raw HTTP messagewith a @Host directive. Use when you need full control over thewire format, want to call a host outside the scan target, or neednon-standard headers / methods.```yamlseed: - protocol: http raw: |
Use the raw HTTP seeder to call an internal-only host (internal.example.com) and alert on a successful response — the production environment is not isolated from internal services.
Multi-environment platforms expect production runtime to be network-isolated from internal/back-office services. When a production-runtime container can resolve and reach internal.example.com, a single SSRF or admin-route mistake becomes a path into the internal network.
The seeder uses the raw HTTP seeder (with the @Host directive) so the request actually leaves the application's normal base_url and goes to the internal hostname.
When to use: Run from your production scanning posture, never from a developer laptop. Replace internal.example.com with the actual hostname you want to assert is unreachable.
OWASP: A05:2021 Security Misconfiguration · CWE: CWE-918
Severity rationale: HIGH when paired with any user-controlled URL fetch in the API; MEDIUM as a standalone configuration finding.
Features used: seed, detect, http raw seeder with @Host directive
rule:
id: example-api-environment-isolation-internal-host
type: API
alert:
name: Production environment can reach internal host
context: |
A request issued to `internal.example.com` from the production
scanner returned a successful response. The runtime
environment is not isolated from internal services.
severity: HIGH
category: REQUEST_FORGERY
seed:
- protocol: http
raw: |
@Host: https://internal.example.com
GET /api/health HTTP/1.1
Host: internal.example.com
detect:
- if: schema.url
contains: internal.example.com
- if: helpers.response.is_successful
is: true
References:
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
- https://cwe.mitre.org/data/definitions/918.html For standard REST endpoints prefer the
restseeder (auto-fillshost and scheme from the scan target). | |raw* |string| | The raw HTTP request in Nuclei format. | |user|string|null| The user to use for the request. If not provided, the request is sent without authentication. |
HelpersRequestCrudDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[helpers.request.crud] | helpers.request.crud | Use this to select against the detected CRUD operation of the request.### Exampleyamldetect: - if: helpers.request.crud in: - CREATE - UPDATE |
in | List[CRUD] | null | Condition is the request is in this list of CRUD operations (exact match) |
is | CRUD | null | Condition is the request is this CRUD operation |
is_not | CRUD | null | Condition is the request is not this CRUD operation |
HelpersResponseIsSuccessfulDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[helpers.response.is_successful] | helpers.response.is_successful | True when the response status code is in the 2xx range.Matcher type: BooleanMatcher · Operators: is````yamldetect: - if: helpers.response.is_successful is: true```In context — combine withhelpers.request.crudandrequest.is_authenticated` to flag anonymous mutations:#### Unauthenticated mutation succeeded |
Detect any successful CREATE / UPDATE / DELETE issued without an authenticated user.
Most APIs gate state-changing operations behind authentication. When a public (unauthenticated) request manages to perform a CREATE / UPDATE / DELETE and receives a 2xx response, the endpoint is missing an authn or authz check.
The rule combines three deterministic signals: the inferred CRUD operation is not READ, the request was issued by the public pseudo-user (Escape's representation of an anonymous caller), and the response is in the 2xx range.
When to use: Apply on every API. This is one of the highest-signal rules in the library and rarely produces false positives because all three conditions must hold simultaneously.
OWASP: API2:2023 Broken Authentication · CWE: CWE-306
Severity rationale: HIGH — anonymous mutations typically allow unauthorized data writes, deletion of resources, or trivial denial-of-service via mass creation.
Features used: detect, helpers.request.crud, request.is_authenticated, helpers.response.is_successful
rule:
id: example-api-unauthenticated-mutation
type: API
alert:
name: Unauthenticated mutation succeeded
context: |
A CREATE / UPDATE / DELETE request issued without authentication
returned a 2xx response. The endpoint is missing an authentication
or authorization check.
severity: HIGH
category: ACCESS_CONTROL
detect:
- if: helpers.request.crud
is_not: READ
- if: request.is_authenticated
is: false
- if: helpers.response.is_successful
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa2-broken-authentication/
- https://cwe.mitre.org/data/definitions/306.html | |
is|boolean|null| Condition is true | |is_not|boolean|null| Condition is false |
JSONMatchesAllDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[helpers.json_matches.all] | helpers.json_matches.all | Use this to determine whether every the current and original responses contain the same JSON fragment. |
is | boolean | null | Condition is true |
is_not | boolean | null | Condition is false |
jq | string | Use this to select the exact JSON you want to compare between the current and original response. |
JSONMatchesCountDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
gt | integer | null | Condition is greater than this integer |
if* | Const[helpers.json_matches.count] | helpers.json_matches.count | Use this to count the number of times a JSON match is in the current and original response. |
in | List[integer] | null | Condition is in this list of integers (exact match) |
is | integer | null | Condition is this exact integer |
is_not | integer | null | Condition is not this exact integer |
jq | string | Use this to select the exact JSON you want to compare between the current and original response. | |
lt | integer | null | Condition is less than this integer |
JsonValue¶
| Property | Type | Default | Description |
|---|---|---|---|
Middleware¶
ObjectMatcher¶
| Property | Type | Default | Description |
|---|---|---|---|
name | StringMatcher | null | Object scalar name to match |
type | ObjectTypeMatcher | null | Object scalar type to match |
value | StringMatcher | null | Object scalar value to match |
ObjectTypeMatcher¶
| Property | Type | Default | Description |
|---|---|---|---|
in | List[OBJECT_TYPE] | null | Object type is in the following list |
is | OBJECT_TYPE | null | Object type is exactly this type |
is_not | OBJECT_TYPE | null | Object type is any this type except this one |
RESTSeeder¶
| Property | Type | Default | Description |
|---|---|---|---|
body | string | null | The body to use for the request. |
headers | Dict[string, string] | null | The headers to use for the request. The key is the header name and the value is the header value. |
method | HTTPMethod | null | The method to use for the request. |
params | Dict[string, string] | null | parameters to use for the request. The key is the parameter name and the value is the parameter value. |
path | string | null | The path to use for the request. |
protocol* | Const[rest] | rest | Inject a REST request at scan start. Host and scheme areautomatically filled from the scan target so you only specify thepath, method, and any headers / body / query parameters.yamlseed: - protocol: rest path: /api/users method: GET user: admin headers: Content-Type: application/json body: '{"name": "tester"}' params: limit: '100'In context — Spring Boot actuator probing:#### Spring Boot actuator /env endpoint exposed |
Probe the Spring Boot actuator /env and /beans endpoints (with and without the /actuator prefix) and alert if the body leaks JAVA_HOME — proof the actuator is reachable.
Spring Boot's actuator exposes deep introspection endpoints (/env, /beans, /heapdump, /configprops, /mappings) that in default 1.x configurations were public. They leak environment variables, database connection strings, JWT secrets, S3 credentials, anything passed via -D flags or env vars.
The rule probes the four most common path layouts and asserts a successful response containing JAVA_HOME, which is virtually certain to be present in /env output.
Sourced from the in-tree springboot_actuator_env simplecheck.
When to use: Any backend potentially using Spring Boot. False-positive rate is very low because the JAVA_HOME marker rarely appears in legitimate API responses.
OWASP: API8:2023 Security Misconfiguration · CWE: CWE-200
Severity rationale: HIGH — actuator output usually contains credentials, signing keys, and infrastructure URLs that lead to direct compromise.
Features used: seed, detect, helpers.response.is_successful, response.body.text contains
rule:
id: example-api-spring-boot-actuator-env
type: API
alert:
name: Spring Boot actuator /env exposed
context: |
A request to the Spring Boot actuator `/env` endpoint returned
a successful response containing `JAVA_HOME`, indicating the
actuator is publicly reachable and leaking environment data.
severity: HIGH
category: INFORMATION_DISCLOSURE
seed:
- protocol: rest
method: GET
path: /actuator/env
headers: {}
- protocol: rest
method: GET
path: /actuator/beans
headers: {}
- protocol: rest
method: GET
path: /env
headers: {}
- protocol: rest
method: GET
path: /beans
headers: {}
detect:
- if: helpers.response.is_successful
is: true
- if: response.body.text
contains: JAVA_HOME
References:
- https://owasp.org/API-Security/editions/2023/en/0xa8-security-misconfiguration/
- https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html
- https://cwe.mitre.org/data/definitions/200.html In context — exposed SQL dump probing with
Rangeheader:#### Exposed SQL dumps reachable over HTTP { #example-api-information-disclosure-exposed-sql-dumps }
Probe a curated list of common SQL dump filenames at the root of the target and alert if the server returns the file with a SQL DDL/DML signature in the body.
Backups left in the web root are one of the highest-impact and cheapest-to-find exposures in API testing. This rule probes a curated list of common backup filenames (backup.sql, dump.sql, db.sql, mysqldump.sql, ...) using a Range: bytes=0-3000 header so the download stays small even if the file is multi-gigabyte. Extend the seed: list with additional paths to suit your target.
Detection requires both a 200/206 status and a body containing typical SQL DDL/DML keywords (DROP TABLE, CREATE TABLE, INSERT INTO, LOCK TABLE). The status code and body regex are AND-combined so a generic 200 page with no SQL content does not fire.
Sourced from the in-tree exposed_sql_dumps simplecheck — promoted here as a canonical example of the seed-+-detect pattern.
When to use: Run against every public host. Especially useful immediately after deployments, when build artifacts are sometimes left in /public by accident.
OWASP: A05:2021 Security Misconfiguration · CWE: CWE-540
Severity rationale: HIGH — exposed dumps regularly contain credentials, password hashes, and full PII.
Features used: seed, detect, rest seeder with custom Range header, response.body.text regex, response.status_code in
rule:
id: example-api-exposed-sql-dumps
type: API
alert:
name: Exposed SQL dump
context: |
A request to a common SQL dump filename returned a 200/206 with
DDL/DML keywords in the body, indicating a database backup
reachable over HTTP.
severity: HIGH
category: INFORMATION_DISCLOSURE
seed:
- protocol: rest
method: GET
path: /backup.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /database.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /dump.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /db.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /mysqldump.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /db_backup.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /wp-content/uploads/dump.sql
headers:
Range: bytes=0-3000
detect:
- if: response.body.text
regex: .*((DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO).*
- if: response.status_code
in:
- 200
- 206
References:
- https://owasp.org/Top10/A05_2021-Security_Misconfiguration/
- https://cwe.mitre.org/data/definitions/540.html | |
user|string|null| The user to use for the request. If not provided, the request is sent without authentication. |
RegexMatchesAllDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[helpers.regex_matches.all] | helpers.regex_matches.all | Use this to determine whether every the current and original responses match the same regular expression. |
is | boolean | null | Condition is true |
is_not | boolean | null | Condition is false |
regex | string | Condition is matched on this regex with fullmatch |
RegexMatchesCountDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
gt | integer | null | Condition is greater than this integer |
if* | Const[helpers.regex_matches.count] | helpers.regex_matches.count | Use this to count the number of times a regex match is in the current and original response. |
in | List[integer] | null | Condition is in this list of integers (exact match) |
is | integer | null | Condition is this exact integer |
is_not | integer | null | Condition is not this exact integer |
lt | integer | null | Condition is less than this integer |
regex | string | Condition is matched on this regex with fullmatch |
RegexReplace¶
| Property | Type | Default | Description |
|---|---|---|---|
pattern* | string | The regex pattern to match. | |
replacement* | string | The replacement, use \1, \2, ... to refer capture groups. | |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
RequestArgument¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
jq | string | null | JQ query to apply to the JSON body. See stedolan.github.io |
key* | Const[request.argument] | request.argument | You can use this extractor to extract an argument from request body as a variable.### ExampleThis example will extract the request argument value if it matches the given scalar and store it in the variable arg.yamlextractors: extract: - key: request.argument variable: 'arg' scalars: - id - uuidThis example will extract the user ID sent in a request body JSON and store it in the variable arg.yamlextractors: extract: - key: request.argument variable: 'arg' jq: '.user.id' |
scalars | List[string] | null | Scalars the argument has to match to be extracted |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
RequestBodyJSONDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[request.body.json] | request.body.json | Use this to select and compare the request body when detected as JSON, using jq-like syntax.### Example 1yamldetect: - if: request.body.json is: id: 42### Example 2yamldetect: - if: request.body.json jq: '.role == admin' |
in | List[JsonValue] | null | Condition is in this list of JSON |
is | JsonValue | null | Condition is this exact JSON |
is_not | JsonValue | null | Condition is not this exact JSON |
jq | string | null | JQ query to match and use as boolean. If use_extraction is True, only this attribute will be parsed (if set). |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
RequestBodyJSONMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
jq | string | null | JQ query to apply to the JSON body. See stedolan.github.io |
key* | Const[request.body.json] | request.body.json | Transform the JSON body of the request using a JQ expression. JQ isthe most powerful mutator in the DSL: it can add fields, removefields, walk every string, restructure objects, etc.Transformation language: JQ — see \<stedolan.github.ioyamlmutate: - key: request.body.json jq: '. + {"is_admin": true, "role": "admin"}'In context — mass assignment via privileged-field injection:#### Mass assignment via role injection in JSON body |
Inject role: admin (and friends) into every successful JSON write issued by a regular user; alert if the modified request still succeeds — the API failed to filter dangerous fields.
Mass assignment (a.k.a. autobinding, BOPLA) happens when an API deserializes a client JSON payload directly into an internal model without a field allow-list. An attacker can then pass extra fields (is_admin, role, verified, subscription_tier, ...) and elevate their own state.
The deterministic test:
transform.triggerselects every successful CREATE / UPDATE issued bytester@example.comwhose JSON body does not yet carry an admin marker.transform.mutateaddsis_admin: trueandrole: adminusing a JQ expression.detectfires when the augmented request is still successful — proving the server accepted the privileged fields.
When to use: Any REST or GraphQL endpoint that accepts a JSON object describing a user-owned resource (profile, account, organization, settings). Adjust the JQ expression to the field names your model uses.
OWASP: API3:2023 BOPLA · CWE: CWE-915
Severity rationale: HIGH — successful mass assignment typically grants full admin impersonation or unbounded subscription upgrades.
Features used: transform, detect, request.body.json mutator (jq), helpers.request.crud
rule:
id: example-api-mass-assignment-role-injection
type: API
alert:
name: Mass assignment accepted privileged fields
context: |
A request augmented with `is_admin: true` and `role: admin` was
accepted by the API. The endpoint deserializes user-supplied
fields into the internal model without a field allow-list.
severity: HIGH
category: ACCESS_CONTROL
transform:
trigger:
- if: helpers.response.is_successful
is: true
- if: helpers.request.crud
in:
- CREATE
- UPDATE
- if: request.user
is: tester@example.com
mutate:
- key: request.body.json
jq: '. + {"is_admin": true, "role": "admin"}'
detect:
- if: helpers.response.is_successful
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
- https://cwe.mitre.org/data/definitions/915.html
- https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html In context — fuzz every string in the body with a SQL-breaking suffix:#### SQL injection in JSON body via JQ-targeted payloads { #example-api-injection-sql-injection-jq-string-fields }
Walk every string field of a successful JSON request body and append a SQL-breaking payload to its value, then alert on a DBMS error in the response.
REST APIs that consume JSON usually deserialize it into typed objects. The classic concatenation-style SQLi sink is then accessible via any user-controlled string field that is passed unvalidated into a query.
The transform uses a JQ expression that walks the JSON structure and rewrites every string leaf by appending ' OR 1=1--. The detector then looks for the same DBMS error fingerprints used by the text-based SQLi rule.
When to use: Any REST or GraphQL endpoint that accepts JSON. Pair with examples/api/injection/sql-injection-error-disclosure.yaml for text-encoded request bodies.
OWASP: A03:2021 Injection · CWE: CWE-89
Severity rationale: HIGH — same impact as classic SQLi.
Features used: transform, detect, request.body.json mutator (jq), response.body.text regex
rule:
id: example-api-sql-injection-jq-string-fields
type: API
alert:
name: SQL injection via JSON string field
context: |
A SQL-breaking suffix appended to every string value in the
request body caused the API to leak a DBMS error string in the
response.
severity: HIGH
category: INJECTION
transform:
trigger:
- if: helpers.response.is_successful
is: true
- if: request.body.json
jq: any(.. | strings)
mutate:
- key: request.body.json
jq: (.. | strings) |= . + "' OR 1=1--"
detect:
- if: response.body.text
regex: .*(syntax error|sql error|ORA-[0-9]+|mysql_fetch|psql:|sqlite3.OperationalError|SQLSTATE\[).*
References:
- https://owasp.org/Top10/A03_2021-Injection/
- https://cwe.mitre.org/data/definitions/89.html Tip: test your JQ queries at \<jqplay.org before using them. | |
use_extraction|boolean|false| If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
RequestBodyTextDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
if* | Const[request.body.text] | request.body.text | Use this to select and compare the request body as text, using string compare.### Exampleyamldetect: - if: request.body.text contains: 'password=' |
in | List[string] | null | Condition is in this list (case-insensitive) |
is | string | null | Condition is this string (case-insensitive) |
is_not | string | null | Condition is not this string (case-insensitive) |
regex | string | null | Condition is matched on this regex with fullmatch (case-insensitive) |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
RequestBodyTextMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
key* | Const[request.body.text] | request.body.text | You can use this mutator to change the body (as text) of the request before resending it.### Exampleyamltransform: trigger: - if: request.body.text contains: 'hello' mutate: - key: request.body.text values: - 'injection 1' - 'injection 2' - 'injection 3' |
regex_replace | RegexReplace | null | Regex replace pattern. |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
value | string | null | The value to set. |
values | List[string] | null | The values to set, generates multiple queries. |
RequestCookieExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[request.cookies] | request.cookies | You can use this extractor to extract variables from the request cookies.### ExampleThis example will extract a session cookie sent in request (cookie) and store it in the variable session.yamlextractors: trigger: - if: response.status_code is: 200 extract: - key: request.cookies name: 'session' variable: 'session' |
name* | string | Cookie name to extract from | |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
RequestHeadersDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[request.headers] | request.headers | Use that to select and compare the request headers in a key value dictionary.### Exampleyamldetect: - if: request.headers key: is: 'X-OPERATION' value: is: 'PAY' |
key | StringMatcher | null | Key to match |
value | StringMatcher | null | Value to match |
RequestHeadersMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
delete | boolean | null | Delete the matched headers. |
key* | Const[request.headers] | request.headers | You can use this mutator to change the headers of the request before resending it. If no header is matched, a new header will be added and set to the value.### Exampleyamltransform: trigger: - if: schema.url is: '/api/v1/tested/route' mutate: - key: request.headers name: X-API-version value: 'APIV2' |
name* | string | The header name to match, supports regex. | |
regex_replace | RegexReplace | null | Regex replace pattern. |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
value | string | null | The value to set. |
values | List[string] | null | The values to set, generates multiple queries. |
RequestIsAuthenticatedDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[request.is_authenticated] | request.is_authenticated | Use this to select whether the request is authenticated.### Exampleyamldetect: - if: request.is_authenticated is: true |
is | boolean | null | Condition is true |
is_not | boolean | null | Condition is false |
RequestMethodDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[request.method] | request.method | Use this to select against the request HTTP Method.### Exampleyamldetect: - if: request.method is: OPTIONS |
in | List[HTTPMethod] | null | Condition is the request is in this list of CRUD operations (exact match) |
is | HTTPMethod | null | Condition is the request is this CRUD operation |
is_not | HTTPMethod | null | Condition is the request is not this CRUD operation |
RequestMethodMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
key* | Const[request.method] | request.method | Change the HTTP method of the request before resending it.yamlmutate: - key: request.method value: DELETE# Multiple methods (one mutated request per value):mutate: - key: request.method values: [POST, PUT, DELETE, PATCH]In context — horizontal privilege escalation via user swap on a write:#### Horizontal privilege escalation via user swap on a write |
Re-issue every successful CREATE / UPDATE / DELETE issued by tester@example.com as attacker@example.com and alert if the write still succeeds — the endpoint is missing horizontal authorization.
Horizontal authorization failures appear when user A can perform a state-changing operation on a resource that belongs to user B. Unlike BOLA (which is read-side), this rule targets writes: delete-someone-else's-record, update-someone-else's-profile, cancel-someone-else's-order.
The trigger picks successful writes by tester@example.com. The mutator switches request.user to attacker@example.com. If the replayed write is still successful, the API never re-checked ownership of the targeted resource against the new caller.
When to use: Any API where users can mutate records keyed by a path/body id. Pair with the platform's authentication configuration: define tester@example.com and attacker@example.com with disjoint resources before enabling this rule.
OWASP: API1:2023 BOLA · CWE: CWE-639
Severity rationale: HIGH — horizontal write escalation typically lets attackers delete or alter records belonging to other tenants and can be weaponized for denial-of-service or sabotage.
Features used: transform, detect, helpers.request.crud, request.user mutator
rule:
id: example-api-horizontal-privilege-escalation-write
type: API
alert:
name: Horizontal privilege escalation on write
context: |
A successful write issued by `tester@example.com` was replayed as
`attacker@example.com` and still succeeded. The endpoint is
missing a horizontal authorization check.
severity: HIGH
category: ACCESS_CONTROL
transform:
trigger:
- if: helpers.response.is_successful
is: true
- if: helpers.request.crud
in:
- CREATE
- UPDATE
- DELETE
- if: request.user
is: tester@example.com
mutate:
- key: request.user
value: attacker@example.com
detect:
- if: helpers.response.is_successful
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- https://cwe.mitre.org/data/definitions/639.html | |
value| HTTPMethod |null| The value to set. | |values|List[HTTPMethod]|null| The values to set, generates multiple queries. |
RequestObjectDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[request.object] | request.object | Use this to select and compare the detected object scalars (including custom scalars) in the request, with their kind, name and value.### Exampleyamldetect: - if: request.object type: in: - email - phone - street_address |
name | StringMatcher | null | Object scalar name to match |
type | ObjectTypeMatcher | null | Object scalar type to match |
value | StringMatcher | null | Object scalar value to match |
RequestObjectMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
key* | Const[request.object] | request.object | The detected object scalars (including custom scalars) in the request, with their kind, name and value.### Exampleyamltransform: trigger: - if: schema.url is: '/api/v1/tested/route' mutate: - key: request.object select: type: is: email name: is: 'admin_email' value: regex: .*@escape.tech mutate: regex_replace: pattern: (.*)@escape.tech replacement: \1@attacker.com |
mutate* | BaseStringMutate | ||
select* | ObjectMatcher |
RequestUserDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
if* | Const[request.user] | request.user | Match the user name attached to the request. The value must match auser defined in your scan authentication settings (the special userpublic represents an unauthenticated caller).Matcher type: StringMatcher · Operators: is, is_not, contains, regex, `in````yamldetect: - if: request.user is: tester@example.com```In context — BOLA detection by replaying as a different user:#### BOLA via user swap with fingerprint comparison |
Replay every successful authenticated request as a different user and flag responses that succeed AND return the same body fingerprint — classic Broken Object Level Authorization.
Broken Object Level Authorization (BOLA / IDOR) is the #1 API risk in the OWASP API Top 10. The pattern is straightforward: endpoint X correctly authenticates tester@example.com and returns its own data, but never re-checks ownership when a different user calls the same URL — so attacker@example.com gets the same response.
This rule encodes the test deterministically:
transform.triggerselects every successful request issued bytester@example.com.transform.mutatere-issues that request withrequest.userset toattacker@example.com.detectfires when the replayed response is also successful AND its body fingerprint matches the original — meaning the API returned the first user's data to the second user.
When to use: Any REST or GraphQL endpoint that returns user-scoped data. Pair with your authentication configuration in the Escape platform: define at least two users with disjoint resources for the fingerprint check to be meaningful.
OWASP: API1:2023 BOLA · CWE: CWE-639
Severity rationale: HIGH — cross-tenant data leakage is typically a reportable security incident and a frequent cause of GDPR / SOC 2 findings.
Features used: transform, detect, request.user mutator, helpers.fingerprints.same, helpers.response.is_successful
rule:
id: example-api-bola-user-swap-fingerprint
type: API
alert:
name: Possible BOLA via user swap
context: |
A successful authenticated response was returned with an identical
body fingerprint when the same request was replayed as a different
user. The endpoint is likely missing an object-level authorization
check.
severity: HIGH
category: ACCESS_CONTROL
transform:
trigger:
- if: helpers.response.is_successful
is: true
- if: request.user
is: tester@example.com
mutate:
- key: request.user
value: attacker@example.com
detect:
- if: helpers.response.is_successful
is: true
- if: helpers.fingerprints.same
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- https://cwe.mitre.org/data/definitions/639.html | |
in|List[string]|null| Condition is in this list (case-insensitive) | |is|string|null| Condition is this string (case-insensitive) | |is_not|string|null| Condition is not this string (case-insensitive) | |regex|string|null| Condition is matched on this regex with fullmatch (case-insensitive) | |use_extraction|boolean|false| If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
RequestUserMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
drop_user | boolean | null | Remove the user authentication from the request. |
key* | Const[request.user] | request.user | Replay the request as a different authenticated user (or as anonymousvia drop_user: true). The user must be defined in your scanauthentication settings.yamlmutate: - key: request.user value: attacker@example.com# Remove authentication entirely:mutate: - key: request.user drop_user: trueIn context — horizontal authorization test on a write:#### Horizontal privilege escalation via user swap on a write |
Re-issue every successful CREATE / UPDATE / DELETE issued by tester@example.com as attacker@example.com and alert if the write still succeeds — the endpoint is missing horizontal authorization.
Horizontal authorization failures appear when user A can perform a state-changing operation on a resource that belongs to user B. Unlike BOLA (which is read-side), this rule targets writes: delete-someone-else's-record, update-someone-else's-profile, cancel-someone-else's-order.
The trigger picks successful writes by tester@example.com. The mutator switches request.user to attacker@example.com. If the replayed write is still successful, the API never re-checked ownership of the targeted resource against the new caller.
When to use: Any API where users can mutate records keyed by a path/body id. Pair with the platform's authentication configuration: define tester@example.com and attacker@example.com with disjoint resources before enabling this rule.
OWASP: API1:2023 BOLA · CWE: CWE-639
Severity rationale: HIGH — horizontal write escalation typically lets attackers delete or alter records belonging to other tenants and can be weaponized for denial-of-service or sabotage.
Features used: transform, detect, helpers.request.crud, request.user mutator
rule:
id: example-api-horizontal-privilege-escalation-write
type: API
alert:
name: Horizontal privilege escalation on write
context: |
A successful write issued by `tester@example.com` was replayed as
`attacker@example.com` and still succeeded. The endpoint is
missing a horizontal authorization check.
severity: HIGH
category: ACCESS_CONTROL
transform:
trigger:
- if: helpers.response.is_successful
is: true
- if: helpers.request.crud
in:
- CREATE
- UPDATE
- DELETE
- if: request.user
is: tester@example.com
mutate:
- key: request.user
value: attacker@example.com
detect:
- if: helpers.response.is_successful
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- https://cwe.mitre.org/data/definitions/639.html In context — BOLA via user swap with fingerprint comparison:#### BOLA via user swap with fingerprint comparison { #example-api-access-control-bola-user-swap-fingerprint }
Replay every successful authenticated request as a different user and flag responses that succeed AND return the same body fingerprint — classic Broken Object Level Authorization.
Broken Object Level Authorization (BOLA / IDOR) is the #1 API risk in the OWASP API Top 10. The pattern is straightforward: endpoint X correctly authenticates tester@example.com and returns its own data, but never re-checks ownership when a different user calls the same URL — so attacker@example.com gets the same response.
This rule encodes the test deterministically:
transform.triggerselects every successful request issued bytester@example.com.transform.mutatere-issues that request withrequest.userset toattacker@example.com.detectfires when the replayed response is also successful AND its body fingerprint matches the original — meaning the API returned the first user's data to the second user.
When to use: Any REST or GraphQL endpoint that returns user-scoped data. Pair with your authentication configuration in the Escape platform: define at least two users with disjoint resources for the fingerprint check to be meaningful.
OWASP: API1:2023 BOLA · CWE: CWE-639
Severity rationale: HIGH — cross-tenant data leakage is typically a reportable security incident and a frequent cause of GDPR / SOC 2 findings.
Features used: transform, detect, request.user mutator, helpers.fingerprints.same, helpers.response.is_successful
rule:
id: example-api-bola-user-swap-fingerprint
type: API
alert:
name: Possible BOLA via user swap
context: |
A successful authenticated response was returned with an identical
body fingerprint when the same request was replayed as a different
user. The endpoint is likely missing an object-level authorization
check.
severity: HIGH
category: ACCESS_CONTROL
transform:
trigger:
- if: helpers.response.is_successful
is: true
- if: request.user
is: tester@example.com
mutate:
- key: request.user
value: attacker@example.com
detect:
- if: helpers.response.is_successful
is: true
- if: helpers.fingerprints.same
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- https://cwe.mitre.org/data/definitions/639.html | |
regex_replace| RegexReplace |null| Regex replace pattern. | |use_extraction|boolean|false| If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. | |value|string|null| The value to set. | |values|List[string]|null| The values to set, generates multiple queries. |
ResponseBodyJSONDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[response.body.json] | response.body.json | Use this to select and compare the response body when detected as JSON, using jq-like syntax.### Example 1yamldetect: - if: response.body.json is: id: 42### Example 2yamldetect: - if: response.body.json jq: '.role == admin' |
in | List[JsonValue] | null | Condition is in this list of JSON |
is | JsonValue | null | Condition is this exact JSON |
is_not | JsonValue | null | Condition is not this exact JSON |
jq | string | null | JQ query to match and use as boolean. If use_extraction is True, only this attribute will be parsed (if set). |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
ResponseBodyJSONExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
jq* | string | JQ query to apply to the JSON body. See stedolan.github.io | |
key* | Const[response.body.json] | response.body.json | You can use this extractor to extract variables from the response body JSON.### ExampleThis example will extract the user ID from the response body JSON and store it in the variable user_id.yamlextractors: trigger: - if: response.status_code is: 200 extract: - key: response.body.json jq: '.user.id' variable: 'user_id' |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
ResponseBodyTextDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
if* | Const[response.body.text] | response.body.text | Use this to select and compare the response body as text, using string compare.### Exampleyamldetect: - if: request.body.text is_not: 'unauthorized' |
in | List[string] | null | Condition is in this list (case-insensitive) |
is | string | null | Condition is this string (case-insensitive) |
is_not | string | null | Condition is not this string (case-insensitive) |
regex | string | null | Condition is matched on this regex with fullmatch (case-insensitive) |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
ResponseBodyTextExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[response.body.text] | response.body.text | You can use this extractor to extract variables from the response body text.### ExampleThis example will extract the response body text and store it in the variable body_data.yamlextractors: trigger: - if: response.status_code is: 200 extract: - key: response.body.text variable: 'body_data' |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
ResponseCookieExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[response.cookies] | response.cookies | You can use this extractor to extract variables from the response cookies.### ExampleThis example will extract a session cookie returned in response (set-cookie) and store it in the variable session.yamlextractors: trigger: - if: response.status_code is: 200 extract: - key: response.cookies name: 'session' variable: 'session' |
name* | string | Cookie name to extract from | |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
ResponseDurationDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
gt | integer | null | Condition is greater than this integer |
if* | Const[response.duration_ms] | response.duration_ms | Use this to compare the duration of the request in milliseconds.### Exampleyamldetect: - if: response.duration_ms gt: 200 |
in | List[integer] | null | Condition is in this list of integers (exact match) |
is | integer | null | Condition is this exact integer |
is_not | integer | null | Condition is not this exact integer |
lt | integer | null | Condition is less than this integer |
ResponseDurationExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[response.duration] | response.duration | You can use this extractor to extract the response duration as a variable.### ExampleThis example will extract the response duration and store it in the variable duration.yamlextractors: extract: - key: response.duration variable: 'duration' |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
ResponseHeaderExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[response.headers] | response.headers | You can use this extractor to extract variables from the response headers.### ExampleThis example will extract a token from the response header X-Token and store it in the variable x_token.yamlextractors: trigger: - if: response.status_code is: 200 extract: - key: response.headers name: 'X-Token' variable: 'x_token' |
name* | string | Header name to extract from | |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
ResponseHeadersDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[response.headers] | response.headers | Use that to select and compare the response headers in a key value dictionary.### Exampleyamldetect: - if: response.headers key: is: 'X-RESULT' value: is: 'PAID' |
key | StringMatcher | null | Key to match |
value | StringMatcher | null | Value to match |
ResponseObjectDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[response.object] | response.object | Use this to select and compare the detected object scalars (including custom scalars) in the response, with their kind, name and value.### Exampleyamldetect: - if: response.object type: in: - email - phone - street_address |
name | StringMatcher | null | Object scalar name to match |
type | ObjectTypeMatcher | null | Object scalar type to match |
value | StringMatcher | null | Object scalar value to match |
ResponseStatusCodeDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
gt | integer | null | Condition is greater than this integer |
if* | Const[response.status_code] | response.status_code | Match against the HTTP response status code.Matcher type: IntegerMatcher · Operators: is, is_not, gt, lt, in````yamldetect: - if: response.status_code in: [200, 206]```For simple success/failure checks preferhelpers.response.is_successful` (it covers the full 2xx range andis more readable).In context — partial-content trick for SQL dump probing:#### Exposed SQL dumps reachable over HTTP |
Probe a curated list of common SQL dump filenames at the root of the target and alert if the server returns the file with a SQL DDL/DML signature in the body.
Backups left in the web root are one of the highest-impact and cheapest-to-find exposures in API testing. This rule probes a curated list of common backup filenames (backup.sql, dump.sql, db.sql, mysqldump.sql, ...) using a Range: bytes=0-3000 header so the download stays small even if the file is multi-gigabyte. Extend the seed: list with additional paths to suit your target.
Detection requires both a 200/206 status and a body containing typical SQL DDL/DML keywords (DROP TABLE, CREATE TABLE, INSERT INTO, LOCK TABLE). The status code and body regex are AND-combined so a generic 200 page with no SQL content does not fire.
Sourced from the in-tree exposed_sql_dumps simplecheck — promoted here as a canonical example of the seed-+-detect pattern.
When to use: Run against every public host. Especially useful immediately after deployments, when build artifacts are sometimes left in /public by accident.
OWASP: A05:2021 Security Misconfiguration · CWE: CWE-540
Severity rationale: HIGH — exposed dumps regularly contain credentials, password hashes, and full PII.
Features used: seed, detect, rest seeder with custom Range header, response.body.text regex, response.status_code in
rule:
id: example-api-exposed-sql-dumps
type: API
alert:
name: Exposed SQL dump
context: |
A request to a common SQL dump filename returned a 200/206 with
DDL/DML keywords in the body, indicating a database backup
reachable over HTTP.
severity: HIGH
category: INFORMATION_DISCLOSURE
seed:
- protocol: rest
method: GET
path: /backup.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /database.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /dump.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /db.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /mysqldump.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /db_backup.sql
headers:
Range: bytes=0-3000
- protocol: rest
method: GET
path: /wp-content/uploads/dump.sql
headers:
Range: bytes=0-3000
detect:
- if: response.body.text
regex: .*((DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO).*
- if: response.status_code
in:
- 200
- 206
References:
- https://owasp.org/Top10/A05_2021-Security_Misconfiguration/
- https://cwe.mitre.org/data/definitions/540.html | |
in|List[integer]|null| Condition is in this list of integers (exact match) | |is|integer|null| Condition is this exact integer | |is_not|integer|null| Condition is not this exact integer | |lt|integer|null| Condition is less than this integer |
ResponseStatusCodeExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[response.status_code] | response.status_code | You can use this extractor to extract the response status code as a variable.### ExampleThis example will extract the response status code and store it in the variable status_code.yamlextractors: extract: - key: response.status_code variable: 'status_code' |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
ScanTypeDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[scan.type] | scan.type | Match against the type of scan being performed.Matcher Type: ScanTypeMatcher · Operators: is, is_not, in · Valid values: REST, `GRAPHQL````yamldetect: - if: scan.type is: REST# ORdetect: - if: scan.type in: [REST, GRAPHQL]``` |
in | List[CustomRuleScanType] | null | The scan type is in this list |
is | CustomRuleScanType | null | The scan type is exactly this |
is_not | CustomRuleScanType | null | The scan type is not this type |
SchemaNeedAuthenticationDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[schema.need_authentication] | schema.need_authentication | Use this to select whether or not the schema requires authentication.### Exampleyamldetect: - if: schema.need_authentication is: false |
is | boolean | null | Condition is true |
is_not | boolean | null | Condition is false |
SchemaPathRefDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
if* | Const[schema.path_ref] | schema.path_ref | Match the operation name (GraphQL) or the path (REST) of the request.Matcher type: StringMatcher · Operators: is, is_not, contains, regex, `in````yamldetect: - if: schema.path_ref contains: /admin/# Or regex over a versioned path:detect: - if: schema.path_ref regex: /api/v[0-9]+/admin/.*```In context — anonymous admin route detection:#### Anonymous request to /admin succeeded |
Flag any successful response on a path containing /admin/ issued without authentication — the cheapest deterministic check for an accidentally-public administrative surface.
Almost no application intentionally exposes admin functionality to anonymous callers. When an endpoint matching /admin/ returns a 2xx to a public request, the most likely explanations are:
- missing authentication middleware,
- a route that bypasses the global auth filter,
- a debug / staging route accidentally promoted to production.
The rule combines three signals (schema.path_ref contains /admin, request is unauthenticated, response is successful) so it fires only when all three hold.
When to use: Any HTTP API. Equally useful as a regression test in CI and as a routine production scan rule. Tighten contains: /admin to a more specific path if your product legitimately exposes anonymous admin metadata pages.
OWASP: API5:2023 Broken Function Level Authorization · CWE: CWE-862
Severity rationale: HIGH — anonymous admin access is reliably exploitable and one of the most common high-severity misconfigurations in API bug-bounty reports.
Features used: detect, schema.path_ref, request.is_authenticated, helpers.response.is_successful
rule:
id: example-api-public-admin-route
type: API
alert:
name: Public administrative route
context: |
An unauthenticated request to a path containing `/admin/`
returned a 2xx response. The route is missing an authentication
check.
severity: HIGH
category: ACCESS_CONTROL
detect:
- if: schema.path_ref
contains: /admin/
- if: request.is_authenticated
is: false
- if: helpers.response.is_successful
is: true
References:
- https://owasp.org/API-Security/editions/2023/en/0xa5-broken-function-level-authorization/
- https://cwe.mitre.org/data/definitions/862.html | |
in|List[string]|null| Condition is in this list (case-insensitive) | |is|string|null| Condition is this string (case-insensitive) | |is_not|string|null| Condition is not this string (case-insensitive) | |regex|string|null| Condition is matched on this regex with fullmatch (case-insensitive) | |use_extraction|boolean|false| If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
SchemaPathRefMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
key* | Const[schema.path_ref] | schema.path_ref | You can use this mutator to change the operation name in GraphQL or the path in REST (keeping the domain) before resending it.### Exampleyamltransform: trigger: - if: schema.path_ref is: '/api/v1/tested/route' mutate: - key: schema.path_ref mutate: value: '/api/v2/tested/route' |
regex_replace | RegexReplace | null | Regex replace pattern. |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
value | string | null | The value to set. |
values | List[string] | null | The values to set, generates multiple queries. |
SchemaUrlDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
if* | Const[schema.url] | schema.url | Use this to string compare the URL of the request.### Example```yamldetect: - if: schema.url regex: .*(internal |
in | List[string] | null | Condition is in this list (case-insensitive) |
is | string | null | Condition is this string (case-insensitive) |
is_not | string | null | Condition is not this string (case-insensitive) |
regex | string | null | Condition is matched on this regex with fullmatch (case-insensitive) |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
SchemaUrlExtractor¶
| Property | Type | Default | Description |
|---|---|---|---|
accept_null | boolean | false | Whether the extractor should accept null values or not. |
can_overwrite | boolean | true | Whether the extractor can overwrite the variable if it already exists. |
key* | Const[schema.url] | schema.url | You can use this extractor to extract variables from the schema URL.### ExampleThis example will extract the schema URL and store it in the variable request_url.yamlextractors: trigger: - if: response.status_code is: 200 extract: - key: schema.url variable: 'request_url' |
variable* | VariableName | The variable name (Case Insensitive) to store the extracted data. |
SchemaUrlMutator¶
| Property | Type | Default | Description |
|---|---|---|---|
key* | Const[schema.url] | schema.url | You can use this mutator to change the URL of the request before resending it.### Exampleyamltransform: trigger: - if: schema.url is: '[api.example.com](https://api.example.com/api/v1/tested/route') mutate: - key: schema.url mutate: value: '[api2.example.com](https://api2.example.com/api/v2/') |
regex_replace | RegexReplace | null | Regex replace pattern. |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
value | string | null | The value to set. |
values | List[string] | null | The values to set, generates multiple queries. |
StringMatcher¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
in | List[string] | null | Condition is in this list (case-insensitive) |
is | string | null | Condition is this string (case-insensitive) |
is_not | string | null | Condition is not this string (case-insensitive) |
regex | string | null | Condition is matched on this regex with fullmatch (case-insensitive) |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
VariableDefinedDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[variable.defined] | variable.defined | Use this to detect if a given variable has been extracted or not yet.### Basic Exampleyamldetect: - if: variable.defined variable_name: user_id### Common Use Cases- Making sure a variable has been already extracted before executing a mutation, a detector or another extraction. |
variable_name* | string | Use this to specify the variable name that has to be defined before proceeding further |
VariableName¶
| Property | Type | Default | Description |
|---|---|---|---|
Enums¶
CRUD¶
| Value |
|---|
CREATE |
READ |
UPDATE |
DELETE |
CustomRuleCategory¶
| Value |
|---|
ACCESS_CONTROL |
CONFIGURATION |
INFORMATION_DISCLOSURE |
INJECTION |
PROTOCOL |
REQUEST_FORGERY |
RESOURCE_LIMITATION |
SENSITIVE_DATA |
SCHEMA |
CUSTOM |
CustomRuleScanType¶
| Value |
|---|
GRAPHQL |
REST |
CustomRuleSeverity¶
| Value |
|---|
HIGH |
MEDIUM |
LOW |
INFO |
HTTPMethod¶
| Value |
|---|
CONNECT |
DELETE |
GET |
HEAD |
OPTIONS |
PATCH |
POST |
PUT |
TRACE |
OBJECT_TYPE¶
| Value |
|---|
See Data Types Reference |
WebApp reference¶
FrontendCustomRule¶
| Property | Type | Default | Description |
|---|---|---|---|
alert* | AlertModel | The alert to raise if the detection conditions are met. See Alerting | |
detect* | List[CookieDetector,DialogMessageDetector,FrontendLogicalAndDetector,FrontendLogicalNotDetector,FrontendLogicalOrDetector,HeaderDetector,JSAssertionDetector,LocalStorageDetector,PageSelectorDetector,PageStatusCodeDetector,PageTextDetector,SessionStorageDetector] | The conditions to trigger the alert. See Detectors | |
extractors* | List[object] | Extractors are not currently supported for Frontend custom rules. | |
id* | CustomRuleID | The unique identifier of the custom rule. It is provided by Escape, do not set it manually. | |
seed* | List[CheckAction,ClickAction,ClickMailMagicLinkAction,FillAction,FillMailTOTPAction,FillTOTPAction,FocusPageAction,GotoAction,SelectAction,SleepAction,SolveCaptchaAction,WaitElementAction,WaitTextAction] | A list of requests to seed the scan. See Seeders | |
type* | Const[WEBAPP] | WEBAPP | The type of the custom rule. It is provided by Escape, do not set it manually. |
Objects¶
CheckAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[check] | check | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
locator* | string | The Playwright Locator to select the checkbox to check | |
select_first_if_multiple | boolean | false | Whether to select the first element if multiple elements are found. If false, an error will be raised. |
timeout | integer | 30 | Timeout in seconds for executing the check action |
ClickAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[click] | click | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
locator* | string | The Playwright Locator to select the element to click on | |
select_first_if_multiple | boolean | false | Whether to select the first element if multiple elements are found. If false, an error will be raised. |
timeout | integer | 30 | Timeout in seconds for executing the click action |
ClickMailMagicLinkAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[click_mail_magic_link] | click_mail_magic_link | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
email_address* | string | The email address where the magic link will be sent.\nMust be a valid email address matching this pattern: {any_text}.{organization_id_short}@scan.escape.tech | |
new_page | boolean | false | Whether to create a new page for the navigation or remain on the current page |
timeout | integer | 60 | The timeout (seconds) to wait for the page to load |
CookieDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[cookie] | cookie | Use this to assert that a cookie is present in the browser.### Exampleyamldetect: - if: cookie key: 'my-key' value: 'pattern' |
key | StringMatcher | null | Key to match |
value | StringMatcher | null | Value to match |
DialogMessageDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this substring (case-insensitive) |
if* | Const[dialog.message] | dialog.message | Use this to assert that a JavaScript dialog (alert, confirm,prompt) was triggered by the page and inspect its message. Usefulfor catching reflected XSS payloads that fire alert() oncerendered.### Exampleyamldetect: - if: dialog.message contains: 'XSS' |
in | List[string] | null | Condition is in this list (case-insensitive) |
is | string | null | Condition is this string (case-insensitive) |
is_not | string | null | Condition is not this string (case-insensitive) |
regex | string | null | Condition is matched on this regex with fullmatch (case-insensitive) |
use_extraction | boolean | false | If True, variable references between {{ }} will be replace with the extracted value (If exists). The string representation of the variable will be used without any extra-processing. |
FillAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[fill] | fill | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
auto_submit | boolean | false | Whether the form should be automatically submitted after the fill action |
locator* | string | The Playwright Locator to select the field to fill | |
one_by_one | boolean | false | Whether the field should be typed one character by one character, like a TOTP code |
select_first_if_multiple | boolean | false | Whether to select the first element if multiple elements are found. If false, an error will be raised. |
timeout | integer | 30 | Timeout in seconds for executing the input filling action |
value* | string | The value to fill in the field |
FillMailTOTPAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[fill_mail_totp] | fill_mail_totp | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
auto_submit | boolean | false | Whether the form should be automatically submitted after the fill action |
email_address* | string | The email address where the TOTP code will be sent.\nMust be a valid email address matching this pattern: {any_text}.{organization_id_short}@scan.escape.tech | |
locator* | string | The Playwright Locator to select the field to fill | |
one_by_one | boolean | false | Whether the field should be typed one character by one character, like a TOTP code |
select_first_if_multiple | boolean | false | Whether to select the first element if multiple elements are found. If false, an error will be raised. |
timeout | integer | 30 | Timeout in seconds for executing the input filling action |
FillTOTPAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[fill_totp] | fill_totp | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
auto_submit | boolean | false | Whether the form should be automatically submitted after the TOTP code is filled |
locator* | string | The Playwright Locator to select the field to fill the TOTP code in | |
secret* | string | The secret to generate the TOTP code from |
FocusPageAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[focus_page] | focus_page | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
url_pattern* | string | The pattern to find in the URL to focus on a page. Should be a regex allowing to match the page using Python re.findall(). |
FrontendLogicalAndDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
and* | List[CookieDetector,DialogMessageDetector,FrontendLogicalAndDetector,FrontendLogicalNotDetector,FrontendLogicalOrDetector,HeaderDetector,JSAssertionDetector,LocalStorageDetector,PageSelectorDetector,PageStatusCodeDetector,PageTextDetector,SessionStorageDetector] | Logical and on a list of detectors | |
if* | Const[and] | and |
FrontendLogicalNotDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[not] | not | |
not | CookieDetector, DialogMessageDetector, FrontendLogicalAndDetector, FrontendLogicalNotDetector, FrontendLogicalOrDetector, HeaderDetector, JSAssertionDetector, LocalStorageDetector, PageSelectorDetector, PageStatusCodeDetector, PageTextDetector, SessionStorageDetector | null | Logical not of a detector |
FrontendLogicalOrDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[or] | or | |
or* | List[CookieDetector,DialogMessageDetector,FrontendLogicalAndDetector,FrontendLogicalNotDetector,FrontendLogicalOrDetector,HeaderDetector,JSAssertionDetector,LocalStorageDetector,PageSelectorDetector,PageStatusCodeDetector,PageTextDetector,SessionStorageDetector] | Logical or on a list of detectors |
GotoAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[goto] | goto | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
new_page | boolean | false | Whether to create a new page for the navigation or remain on the current page |
timeout | integer | 60 | The timeout (seconds) to wait for the page to load |
url* | string | The URL to navigate to |
HeaderDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[header] | header | Use this to assert that a header is present in the request.### Exampleyamldetect: - if: header key: 'my-key' value: 'pattern' |
key | StringMatcher | null | Key to match |
value | StringMatcher | null | Value to match |
JSAssertionDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
command | string | null | JavaScript command to execute |
if* | Const[js_assertion] | js_assertion | Use this to execute a JavaScript command and assert it returns true.### Exampleyamldetect: - if: js_assertion command: 'return window.isAuthenticated === false;' |
LocalStorageDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[local_storage] | local_storage | Use this to assert that a key is present in the local storage.### Exampleyamldetect: - if: local_storage key: 'my-key' value: 'pattern' |
key | StringMatcher | null | Key to match |
value | StringMatcher | null | Value to match |
PageSelectorDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this string |
if* | Const[page_selector] | page_selector | Use this to assert that a selector exists in the DOM.### Exampleyamldetect: - if: page_selector contains: '#my-element' |
PageStatusCodeDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
gt | integer | null | Condition is greater than this integer |
if* | Const[page_status_code] | page_status_code | Use this to assert that the page status code is a specific value.### Exampleyamldetect: - if: page_status_code status_code: 200 |
in | List[integer] | null | Condition is in this list of integers (exact match) |
is | integer | null | Condition is this exact integer |
is_not | integer | null | Condition is not this exact integer |
lt | integer | null | Condition is less than this integer |
PageTextDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
contains | string | null | Contains this string |
if* | Const[page_text] | page_text | Use this to assert that a text is present in the page.### Exampleyamldetect: - if: page_text contains: 'Hello, world!' |
SelectAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[select] | select | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
locator* | string | The Playwright Locator to select the dropdown to select from | |
select_first_if_multiple | boolean | false | Whether to select the first element if multiple elements are found. If false, an error will be raised. |
timeout | integer | 30 | Timeout in seconds for executing the click action |
value* | string | The value to select |
SessionStorageDetector¶
| Property | Type | Default | Description |
|---|---|---|---|
if* | Const[session_storage] | session_storage | Use this to assert that a key is present in the session storage.### Exampleyamldetect: - if: session_storage key: 'my-key' value: 'pattern' |
key | StringMatcher | null | Key to match |
value | StringMatcher | null | Value to match |
SleepAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[sleep] | sleep | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
seconds* | number | The time to sleep in seconds, between 0 and 40 seconds |
SolveCaptchaAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[solve_captcha] | solve_captcha | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
auto_submit | boolean | false | Whether the form should be automatically submitted after the captcha is filled |
locator* | string | The Playwright Locator to select the captcha field to fill |
WaitElementAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[wait_element] | wait_element | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
locator* | string | The selector to wait for | |
timeout | number | 10 | The timeout (seconds) to wait for the element to be visible |
WaitTextAction¶
| Property | Type | Default | Description |
|---|---|---|---|
action* | Const[wait_text] | wait_text | |
allow_failure | boolean | false | Allow this action to fail without breaking authentication, defaults to False. |
timeout | number | 10 | The timeout (seconds) to wait for the text to be visible |
value* | string | The text to wait for until visible, case-insensitive |