Skip to content

Examples for APIs (43)

This page lists every canonical API custom rule example shipped with Escape (43 total), grouped by the vulnerability category it targets. Each example is a complete, anonymized rule that round-trips through the SaaS schema. Copy the rule: block, edit hostnames and users to your scan target, and ship.

Categories

Access Control (9)

State-changing request without a CSRF token header

Flag any successful CREATE / UPDATE / DELETE that did not carry a CSRF token header, so cookie-authenticated browsers can be coerced into issuing it cross-origin.

Cookie-based authentication remains the dominant mechanism for server-rendered apps. Any state-changing endpoint that does not require a CSRF token (X-CSRF-Token, X-XSRF-Token, X-Requested-With, ...) can be triggered by a malicious origin via <form> POST or <img> GET-with-side-effects.

The rule is purely passive: no transform, no seed. It greps every successful mutation request for a CSRF-style header and alerts when none is present.

When to use: Any web API that accepts cookies for authentication. Adjust the or branch of headers if your platform standardizes on a different header name.

OWASP: A01:2021 Broken Access Control (CSRF) · CWE: CWE-352

Severity rationale: MEDIUM — exploitation requires luring an authenticated victim, but the impact (state change in their account) is direct and deterministic.

Features used: detect, helpers.request.crud, request.headers, request.is_authenticated, logical not, logical or

rule:
  id: example-api-missing-csrf-token
  type: API
  alert:
    name: State-changing request missing CSRF token
    context: |
      A successful CREATE / UPDATE / DELETE was accepted from an
      authenticated session without any CSRF-style header. The endpoint
      is exposed to cross-site request forgery from cookie-bearing
      victims.
    severity: MEDIUM
    category: REQUEST_FORGERY
  detect:
  - if: helpers.request.crud
    is_not: READ
  - if: request.is_authenticated
    is: true
  - if: helpers.response.is_successful
    is: true
  - if: not
    not:
      if: or
      or:
      - if: request.headers
        key:
          is: X-CSRF-Token
      - if: request.headers
        key:
          is: X-XSRF-Token
      - if: request.headers
        key:
          is: X-Requested-With

References:


BOLA via numeric ID enumeration

Detect Broken Object Level Authorization on resources whose URL ends in a numeric ID by replaying every successful GET as the same user with the path id incremented to enumerate someone else's record.

Many APIs expose object-scoped routes such as /api/orders/{id} where {id} is a small monotonically-increasing integer. When the server forgets to verify that the calling user owns the requested object, an attacker can enumerate ?id=1, ?id=2, ... to walk the full resource set.

The deterministic test rewrites the path id to a constant unrelated integer (999999) using a regex replace mutator and asserts the response is still successful. False positives are rare because the target user almost certainly does not own that exact id.

When to use: Any REST endpoint with a numeric integer in the path (/users/123, /orders/456/items/789, ...). Adjust the schema.path_ref regex and the replacement id to your target.

OWASP: API1:2023 BOLA · CWE: CWE-639

Severity rationale: HIGH — successful enumeration usually allows attackers to harvest the full customer dataset, an event that is reportable under most privacy regulations.

Features used: transform, detect, schema.path_ref mutator (regex_replace), helpers.response.is_successful

rule:
  id: example-api-bola-numeric-id-swap
  type: API
  alert:
    name: BOLA via numeric ID enumeration
    context: |
      A successful response was returned when the resource id in the URL
      was rewritten to an unrelated integer. The endpoint is missing an
      object-level authorization check.
    severity: HIGH
    category: ACCESS_CONTROL
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: schema.path_ref
      regex: .*/[0-9]+(/.*)?
    mutate:
    - key: schema.path_ref
      regex_replace:
        pattern: /[0-9]+(/|$)
        replacement: /999999\1
  detect:
  - if: helpers.response.is_successful
    is: true

References:


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:

  1. transform.trigger selects every successful request issued by tester@example.com.
  2. transform.mutate re-issues that request with request.user set to attacker@example.com.
  3. detect fires 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:


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:


IDOR via extracted ID re-injection across users

Extract a resource id created by tester@example.com then re-issue the read of that same id as attacker@example.com. Alert if the second user gets a successful response.

This pattern automates the canonical IDOR test:

  1. The extractors block listens for any successful response from tester@example.com on a write endpoint and captures .id from the JSON body into the victim_resource_id variable.
  2. The transform block triggers on subsequent successful reads by attacker@example.com whose URL contains /{{victim_resource_id}}. It rewrites the request user back to attacker@example.com (a no-op here, but kept so the same shape can be extended to swap users when needed).
  3. detect fires when attacker@example.com successfully reads a resource whose id was originally returned to a different user.

The variable templating relies on use_extraction: true to expand {{victim_resource_id}} at evaluation time.

When to use: APIs whose CREATE responses return the new resource id and whose READ routes accept that id directly in the URL (/orders/{id}, /comments/{id}, ...). This is the most common shape of IDOR in REST APIs.

OWASP: API1:2023 BOLA · CWE: CWE-639

Severity rationale: HIGH — direct cross-user reads of arbitrary objects.

Features used: extractors, transform, detect, response.body.json (jq) extractor, schema.url with use_extraction

rule:
  id: example-api-idor-extract-and-replay
  type: API
  alert:
    name: IDOR — cross-user read of an extracted resource id
    context: |
      `attacker@example.com` successfully read a resource whose id was
      originally returned to `tester@example.com`. The endpoint is
      missing an object-level authorization check.
    severity: HIGH
    category: ACCESS_CONTROL
  extractors:
  - trigger:
    - if: request.user
      is: tester@example.com
    - if: helpers.response.is_successful
      is: true
    - if: helpers.request.crud
      is: CREATE
    extract:
    - key: response.body.json
      jq: .id
      variable: victim_resource_id
      can_overwrite: true
      accept_null: false
  detect:
  - if: variable.defined
    variable_name: victim_resource_id
  - if: request.user
    is: attacker@example.com
  - if: schema.url
    use_extraction: true
    contains: /{{victim_resource_id}}
  - if: helpers.response.is_successful
    is: true

References:


JWT alg=none accepted

Replay every successful authenticated request with an unsigned alg: none JWT in Authorization. Alert if the response is still successful — the verifier accepted an unsigned token.

A JWT verifier that accepts alg: none lets any caller forge arbitrary identity claims. The deterministic test substitutes the real bearer token with a hand-crafted unsigned JWT carrying the same subject claim and re-issues the request.

The unsigned token below decodes to {"alg":"none","typ":"JWT"}.{"sub":"tester@example.com"}. (note the trailing dot and empty signature segment).

A successful 2xx with this token proves the server skipped signature validation when alg=none.

When to use: Any REST or GraphQL API that uses JWT bearer tokens. Replace the sub claim with a real user identifier from your authentication configuration to make the test meaningful.

OWASP: API2:2023 Broken Authentication · CWE: CWE-347

Severity rationale: CRITICAL when exploitable — accepting alg=none is full identity forgery and grants instant impersonation of any user.

Features used: transform, detect, request.headers mutator, helpers.response.is_successful

rule:
  id: example-api-jwt-alg-none
  type: API
  alert:
    name: JWT alg=none accepted by the API
    context: |
      The API returned a successful response when the bearer token was
      replaced with an unsigned `alg: none` JWT. The verifier is
      misconfigured and trusts forged tokens.
    severity: HIGH
    category: ACCESS_CONTROL
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.is_authenticated
      is: true
    - if: request.headers
      key:
        is: Authorization
      value:
        regex: (?i)^bearer\s.+
    mutate:
    - key: request.headers
      name: Authorization
      value: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ0ZXN0ZXJAZXhhbXBsZS5jb20ifQ.
  detect:
  - if: helpers.response.is_successful
    is: true

References:


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:


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:

  1. transform.trigger selects every successful CREATE / UPDATE issued by tester@example.com whose JSON body does not yet carry an admin marker.
  2. transform.mutate adds is_admin: true and role: admin using a JQ expression.
  3. detect fires 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:


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:


Configuration (6)

Missing Strict-Transport-Security header

Alert on any successful HTTPS response that does not carry an HSTS header — the browser is not pinned to HTTPS for subsequent requests.

HTTP Strict Transport Security (HSTS) instructs browsers to refuse plain-HTTP connections to a host for the configured duration. Without HSTS, an attacker on the network path can SSL-strip the very first request after a cold cache.

The detector requires the request to have used HTTPS (otherwise HSTS would be advisory) and asserts the response carries a Strict-Transport-Security header with at least a one-year max-age.

When to use: Any API or web app served over HTTPS. Adjust the regex to your minimum acceptable max-age (one year is the OWASP-recommended floor).

OWASP: A02:2021 Cryptographic Failures · CWE: CWE-319

Severity rationale: LOW — the attack requires network-path adversary positioning.

Features used: detect, schema.url, response.headers, logical not

rule:
  id: example-api-missing-hsts
  type: API
  alert:
    name: Missing HSTS header
    context: |
      A successful HTTPS response was returned without a valid
      `Strict-Transport-Security` header. The browser is not pinned
      to HTTPS for subsequent navigations.
    severity: LOW
    category: CONFIGURATION
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: schema.url
    regex: ^https://.*
  - if: not
    not:
      if: response.headers
      key:
        is: Strict-Transport-Security
      value:
        regex: .*max-age=([3-9][0-9]{6}|[1-9][0-9]{7,}).*

References:


CORS reflects arbitrary origin with credentials

Replay every authenticated request with Origin: https://attacker.example and alert if the response echoes the same value in Access-Control-Allow-Origin together with Access-Control-Allow-Credentials: true.

Origin reflection is more dangerous than the static wildcard: the server echoes whatever Origin it sees and pairs it with Allow-Credentials: true, so attacker-controlled pages can read authenticated responses with the victim's cookies attached.

The deterministic test:

  1. transform.trigger selects every successful authenticated request that carries an Origin header (i.e. browser-issued).
  2. transform.mutate rewrites Origin to https://attacker.example.
  3. detect fires when the response carries Access-Control-Allow-Origin: https://attacker.example AND Access-Control-Allow-Credentials: true.

When to use: Any API exposed to browsers with cookie or bearer authentication.

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-942

Severity rationale: HIGH — direct cross-origin theft of authenticated response bodies; routinely escalated to full account takeover.

Features used: transform, detect, request.headers mutator, response.headers

rule:
  id: example-api-cors-origin-reflection
  type: API
  alert:
    name: CORS reflects arbitrary origin with credentials
    context: |
      The API echoed an attacker-supplied `Origin` value in
      `Access-Control-Allow-Origin` together with
      `Access-Control-Allow-Credentials: true`, allowing any origin
      to read authenticated response bodies.
    severity: HIGH
    category: CONFIGURATION
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.is_authenticated
      is: true
    - if: request.headers
      key:
        is: Origin
    mutate:
    - key: request.headers
      name: Origin
      value: https://attacker.example
  detect:
  - if: response.headers
    key:
      is: Access-Control-Allow-Origin
    value:
      is: https://attacker.example
  - if: response.headers
    key:
      is: Access-Control-Allow-Credentials
    value:
      is: 'true'

References:


CORS Access-Control-Allow-Origin wildcard

Alert on any response that returns Access-Control-Allow-Origin: * on an authenticated route — every origin can read the response body.

Access-Control-Allow-Origin: * is acceptable on truly public APIs but lethal anywhere a session cookie or bearer token is used: it lets every origin read the response body via XHR / fetch.

The detector fires only when the request was authenticated AND the response carries the wildcard, which is the unambiguous misconfiguration. Pure unauthenticated APIs that legitimately serve every origin are not flagged.

When to use: Any API. Combine with the cors-origin-reflection.yaml rule for full CORS coverage (reflection is more dangerous because it bypasses the wildcard's withCredentials ban).

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-942

Severity rationale: MEDIUM — straightforward exfiltration of authenticated response bodies from any web origin.

Features used: detect, request.is_authenticated, response.headers

rule:
  id: example-api-cors-wildcard
  type: API
  alert:
    name: CORS wildcard on authenticated route
    context: |
      An authenticated API response was returned with
      `Access-Control-Allow-Origin: *`, exposing the response body
      to every web origin.
    severity: MEDIUM
    category: CONFIGURATION
  detect:
  - if: request.is_authenticated
    is: true
  - if: helpers.response.is_successful
    is: true
  - if: response.headers
    key:
      is: Access-Control-Allow-Origin
    value:
      is: '*'

References:


Exposed .env file

Probe /.env, /.env.local, /.env.production and alert if the body looks like a dotenv file (KEY=VALUE lines, common keys).

.env files routinely contain DB passwords, OAuth secrets, Stripe keys, and SMTP credentials. They end up in the web root when deploys ship the project source verbatim or when a build step copies the development env file to dist/.

The detector requires both a successful response AND a body containing typical dotenv keys (DB_PASSWORD, DATABASE_URL, SECRET_KEY, AWS_ACCESS_KEY_ID). False positives are essentially impossible.

When to use: Any web property. Run after every deployment.

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-538

Severity rationale: HIGH — exposed env files are a direct credential leak.

Features used: seed, detect, response.body.text contains, logical or

rule:
  id: example-api-exposed-env-file
  type: API
  alert:
    name: Exposed .env file
    context: |
      A request to `/.env` (or a known variant) returned a body
      containing dotenv markers, indicating environment configuration
      with secrets is publicly downloadable.
    severity: HIGH
    category: CONFIGURATION
  seed:
  - protocol: rest
    method: GET
    path: /.env
  - protocol: rest
    method: GET
    path: /.env.local
  - protocol: rest
    method: GET
    path: /.env.production
  - protocol: rest
    method: GET
    path: /.env.development
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: or
    or:
    - if: response.body.text
      contains: DB_PASSWORD
    - if: response.body.text
      contains: DATABASE_URL
    - if: response.body.text
      contains: SECRET_KEY
    - if: response.body.text
      contains: AWS_ACCESS_KEY_ID

References:


Missing X-Frame-Options on HTML responses

Alert on every HTML response that does not carry an X-Frame-Options (or equivalent CSP frame-ancestors) header — the page is clickjackable.

Pages that lack X-Frame-Options: DENY (or the modern Content-Security-Policy: frame-ancestors 'none') can be embedded in an attacker-controlled iframe and used for clickjacking. The trigger filters for HTML responses (the only ones for which the header is meaningful) and the detector fires when both header families are absent.

When to use: Any web app that returns HTML. Especially important on authenticated pages where clickjacking can trick a logged-in user into clicking a privileged action.

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-1021

Severity rationale: LOW for unauthenticated pages, MEDIUM when applied to authenticated dashboards. This rule defaults to LOW.

Features used: detect, response.headers, logical not, logical or

rule:
  id: example-api-missing-x-frame-options
  type: API
  alert:
    name: Missing X-Frame-Options
    context: |
      An HTML response was returned without `X-Frame-Options` and
      without a `Content-Security-Policy` declaring
      `frame-ancestors`. The page can be embedded in a malicious
      iframe and used for clickjacking.
    severity: LOW
    category: CONFIGURATION
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.headers
    key:
      is: Content-Type
    value:
      regex: .*text/html.*
  - if: not
    not:
      if: or
      or:
      - if: response.headers
        key:
          is: X-Frame-Options
      - if: response.headers
        key:
          is: Content-Security-Policy
        value:
          regex: .*frame-ancestors.*

References:


API version drift — V2 endpoint accepts V1 token

Replay every successful V2 request with the API version header rewritten to V1 and alert if the response is still successful — the API exposes a deprecated V1 contract on the V2 surface.

Long-lived APIs accumulate version drift: the V1 contract is deprecated in docs but the runtime still routes a request with X-API-Version: V1 to the legacy handler — frequently with weaker authorization.

The transform mutates X-API-Version from V2 to V1; if the response is still successful, the V2 endpoint is silently backwards-compatible with V1 requests, which is almost certainly not the intent.

When to use: APIs that explicitly advertise a X-API-Version (or Accept-Version, Api-Version) header. Tighten the trigger to your routes if your version header is named differently.

OWASP: API9:2023 Improper Inventory Management · CWE: CWE-672

Severity rationale: LOW for inventory hygiene, MEDIUM if the V1 surface bypasses authorization (likely; that's why it was deprecated).

Features used: transform, detect, request.headers mutator

rule:
  id: example-api-version-drift-v2-accepts-v1
  type: API
  alert:
    name: V2 endpoint silently accepts V1 token
    context: |
      A successful V2 request continued to succeed when the
      `X-API-Version` header was downgraded to V1, indicating the
      deprecated V1 contract is still reachable on the V2 endpoint.
    severity: LOW
    category: CONFIGURATION
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.headers
      key:
        is: X-API-Version
      value:
        is: V2
    mutate:
    - key: request.headers
      name: X-API-Version
      value: V1
  detect:
  - if: helpers.response.is_successful
    is: true

References:


Information Disclosure (10)

Exposed phpinfo() page

Probe /phpinfo.php and friends and alert if the response carries the unmistakable PHP Version heading — a phpinfo dump leaks paths, modules, env vars, and DB credentials.

phpinfo() was historically used to verify a PHP install, then promptly forgotten. A live /phpinfo.php page reveals:

  • PHP version + module list (input for known-CVE matching),
  • INI directives (open_basedir, disable_functions),
  • environment variables (frequently containing DB and S3 credentials),
  • filesystem paths (DOCROOT, includes search path).

The detector matches the canonical PHP Version heading. False positives are essentially impossible.

When to use: Any PHP application. Run after deployments since debugging pages are routinely committed during refactors.

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-200

Severity rationale: HIGH — phpinfo dumps regularly contain credentials and always speed up exploit chain construction.

Features used: seed, detect, response.body.text contains, helpers.response.is_successful

rule:
  id: example-api-exposed-phpinfo
  type: API
  alert:
    name: phpinfo() exposed
    context: |
      A request to a PHP debug filename returned a page containing
      `PHP Version`, indicating a `phpinfo()` dump is publicly
      reachable.
    severity: HIGH
    category: INFORMATION_DISCLOSURE
  seed:
  - protocol: rest
    method: GET
    path: /phpinfo.php
  - protocol: rest
    method: GET
    path: /info.php
  - protocol: rest
    method: GET
    path: /test.php
  - protocol: rest
    method: GET
    path: /_phpinfo.php
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.body.text
    regex: .*<title>phpinfo\(\).*</title>.*

References:


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:


WordPress XML-RPC endpoint exposed

POST a system.listMethods call to /xmlrpc.php and alert if it succeeds — XML-RPC is enabled and supports brute-forceable login methods.

The WordPress XML-RPC endpoint (/xmlrpc.php) is enabled by default and exposes APIs (wp.getUsersBlogs, system.multicall) that can be used to brute-force credentials at thousands of attempts per request, bypassing typical login-page rate limits.

The seeder issues a benign system.listMethods request. A 2xx XML response means the endpoint is live and exploitable; sites should disable it via a firewall rule or add_filter('xmlrpc_enabled', '__return_false').

Sourced from the in-tree wordpress_xmlrpc_php_exposed simplecheck.

When to use: Any WordPress site. Particularly important on multi-author publishing platforms where system.multicall becomes a brute force amplifier.

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-307

Severity rationale: MEDIUM — endpoint exposure alone is configuration debt; the HIGH upgrade comes when paired with a successful credential probe.

Features used: seed, detect, helpers.response.is_successful, schema.path_ref

rule:
  id: example-api-wordpress-xmlrpc-exposed
  type: API
  alert:
    name: WordPress XML-RPC endpoint exposed
    context: |
      A `system.listMethods` POST to `/xmlrpc.php` returned a
      successful response, indicating the XML-RPC endpoint is enabled
      and supports brute-forceable login methods.
    severity: MEDIUM
    category: INFORMATION_DISCLOSURE
  seed:
  - protocol: rest
    method: POST
    path: /xmlrpc.php
    headers:
      Content-Type: text/xml
    body: |
      <?xml version="1.0"?>
      <methodCall>
        <methodName>system.listMethods</methodName>
      </methodCall>
  detect:
  - if: schema.path_ref
    is: /xmlrpc.php
  - if: helpers.response.is_successful
    is: true

References:


WordPress REST API user enumeration

Probe /wp/users and alert on a successful response — the WP REST API leaks the user list (id, slug, name) without authentication by default.

The WordPress REST API endpoint /wp-json/wp/v2/users (or /wp/users after a rewrite) returns the full author list to anonymous callers in default installs. The response includes slug, which is the username an attacker needs for brute force.

Sourced from the in-tree wordpress_rest_api_users_exposed simplecheck.

When to use: Any WordPress site. Disable via add_filter('rest_endpoints', …) or block at the WAF / reverse-proxy.

OWASP: A07:2021 Identification and Authentication Failures · CWE: CWE-200

Severity rationale: LOW alone — but combined with /wp-login.php exposure or xmlrpc.php, it creates a complete brute-force prerequisite chain.

Features used: seed, detect, schema.path_ref, helpers.response.is_successful

rule:
  id: example-api-wordpress-rest-users-enum
  type: API
  alert:
    name: WordPress REST API users endpoint exposed
    context: |
      `/wp/users` returned a successful response without
      authentication, leaking the WordPress author list (id, slug,
      name) to anonymous callers.
    severity: LOW
    category: INFORMATION_DISCLOSURE
  seed:
  - protocol: rest
    method: GET
    path: /wp/users
  detect:
  - if: schema.path_ref
    is: /wp/users
  - if: helpers.response.is_successful
    is: true

References:


Java stack trace disclosed in API response

Alert on any 5xx response whose body contains a Java stack trace fingerprint (at com., Caused by:, org.springframework, java.lang.).

Verbose error responses are an information disclosure mainstay: they reveal the framework (Spring, JPA, Jackson), the package structure, internal hostnames, and frequently the exact SQL/SQL-builder that failed.

The detector grep is conservative: it requires a multi-substring match (the keyword Caused by: together with at least one of the common package prefixes) so legitimate textual mentions of "java.lang" don't fire.

When to use: Any Java-based API. Useful both as a routine prod scan rule and as a CI gate to prevent stack traces from being committed to error-page responses.

OWASP: A09:2021 Security Logging and Monitoring Failures · CWE: CWE-209

Severity rationale: LOW alone — the exposure aids reconnaissance but is rarely directly exploitable. Aggregates with other findings to elevate severity.

Features used: detect, response.status_code, response.body.text contains, logical or

rule:
  id: example-api-java-stack-trace
  type: API
  alert:
    name: Java stack trace disclosed
    context: |
      The API returned a 5xx response containing a Java stack trace,
      revealing the framework, package layout, and internal class
      names.
    severity: LOW
    category: INFORMATION_DISCLOSURE
  detect:
  - if: response.status_code
    gt: 499
  - if: response.body.text
    contains: 'Caused by:'
  - if: or
    or:
    - if: response.body.text
      contains: at com.
    - if: response.body.text
      contains: at org.springframework
    - if: response.body.text
      contains: java.lang.

References:


Sensitive field names leaked in JSON response

JQ-walk every successful JSON response for keys whose name implies a secret (password, secret, token, api_key, private_key, ssn) and alert when any is present and non-null.

APIs frequently leak server-side fields through over-eager serialization (User.toJson() returning the password hash, S3 presigners returning the AWS secret, OAuth callbacks returning the client secret).

The deterministic test scans every response body with a single JQ query that returns true if any field whose key matches a sensitive name carries a non-null value.

The query uses walk over .. | objects so it finds the field at any nesting depth.

When to use: Any API that returns JSON. Add or remove keys from the regex according to your data dictionary; the rule is most powerful when you tailor it to fields you know should never leave the server.

OWASP: API3:2023 BOPLA · CWE: CWE-200

Severity rationale: HIGH — direct credential / secret leak; treat as if the leaked value had been pasted into a public Slack channel.

Features used: detect, response.body.json (jq)

rule:
  id: example-api-sensitive-fields-jq
  type: API
  alert:
    name: Sensitive field present in API response
    context: |
      The response body contains a non-null value at a key whose name
      implies a secret (`password`, `secret`, `token`, `api_key`,
      `private_key`, `ssn`). The endpoint is over-serializing
      sensitive fields.
    severity: HIGH
    category: SENSITIVE_DATA
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.body.json
    jq: |
      any(.. | objects | to_entries[]?;
          (.key | test("(?i)^(password|secret|token|api[_-]?key|private[_-]?key|ssn|client[_-]?secret)$"))
          and (.value != null and .value != ""))

References:


Debug headers reinjected and surfaced in alert context

Add a Pragma: show-debug-headers header to every successful request and alert when the response leaks an internal application name via Cgp-Route-Ams-Application-Name, embedding the leaked value in the alert context using an extractor.

Some reverse proxies and routing layers honor magic headers (here Pragma: show-debug-headers) and leak internal routing or application metadata. Discovery is two-step:

  1. transform injects the magic header into every successful non-/graphql request.
  2. detect fires when the response carries Cgp-Route-Ams-Application-Name.
  3. extractors capture the leaked value into show_headers_value so the alert context can render the actual exposed name — turning a generic finding into actionable evidence.

The rule is a reduced, anonymized variant of an internal Escape detection that has caught the issue in production scans.

When to use: Any REST API behind a load balancer or service mesh. Replace the header name in transform.mutate and the detect/extract path with the magic header your platform actually honors.

OWASP: API8:2023 Security Misconfiguration · CWE: CWE-200

Severity rationale: INFO by default — the leak rarely allows direct exploitation, but it enables targeted follow-up attacks against the named internal service.

Features used: transform, detect, extractors, request.headers mutator, response.headers detector, alert context templating

rule:
  id: example-api-debug-header-application-name
  type: API
  alert:
    name: Debug header exposes internal application name
    context: |
      The endpoint returned internal routing info via debug headers.
      Cgp-Route-Ams-Application-Name = {{show_headers_value}}
    severity: INFO
    category: INFORMATION_DISCLOSURE
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: schema.url
      regex: ^(?!.*/graphql).*$
    mutate:
    - key: request.headers
      name: Pragma
      value: show-debug-headers
  detect:
  - if: response.headers
    key:
      is: Cgp-Route-Ams-Application-Name
  - if: schema.url
    regex: ^(?!.*/graphql).*$
  extractors:
  - trigger:
    - if: response.headers
      key:
        is: Cgp-Route-Ams-Application-Name
    extract:
    - key: response.headers
      name: Cgp-Route-Ams-Application-Name
      variable: show_headers_value
      can_overwrite: true

References:


GraphQL introspection enabled in production

Send the canonical introspection query to /graphql and alert if it returns a __schema object — introspection is enabled and leaks the full schema.

GraphQL introspection lets clients query the schema itself. It is indispensable in development (powers GraphiQL and codegen) and almost always meant to be disabled in production. A live introspection endpoint hands attackers the entire type graph, including type names that hint at internal admin operations.

The seeder posts the standard introspection query. The detector looks for __schema in the response body, which appears in every introspection result and almost never in any other response.

When to use: Any GraphQL endpoint. False-positive rate is essentially zero.

OWASP: API8:2023 Security Misconfiguration · CWE: CWE-200

Severity rationale: MEDIUM — schema disclosure is reconnaissance, not direct compromise; impact escalates when paired with overly-broad mutations discoverable via the leaked schema.

Features used: seed, detect, response.body.text contains

rule:
  id: example-api-graphql-introspection-public
  type: API
  alert:
    name: GraphQL introspection enabled
    context: |
      `POST /graphql` returned a successful response containing
      `__schema`, indicating GraphQL introspection is enabled and the
      schema is publicly readable.
    severity: MEDIUM
    category: INFORMATION_DISCLOSURE
  seed:
  - protocol: rest
    method: POST
    path: /graphql
    headers:
      Content-Type: application/json
    body: '{"query":"{ __schema { types { name } } }"}'
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.body.text
    contains: __schema

References:


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:


Spring Boot heapdump downloadable

Probe the actuator /heapdump and /mappings endpoints and alert if the response contains heapdump and org.springframework, indicating a downloadable JVM heap dump.

A downloadable heapdump is one of the worst Spring Boot exposures. Heapdumps contain in-memory state — DB connection strings, OAuth secrets, customer PII actively being processed, JWT signing keys, everything the JVM is currently holding.

The detector requires both fingerprint substrings to fire, which suppresses generic 200 responses with no heap content.

Sourced from the in-tree springboot_actuator_heapdump simplecheck.

When to use: Any Spring Boot backend. Run early in a scan because a confirmed heapdump exposure usually moots the rest of the test plan.

OWASP: API8:2023 Security Misconfiguration · CWE: CWE-200

Severity rationale: HIGH — full memory disclosure is reliably weaponized into credential theft and lateral movement.

Features used: seed, detect, helpers.response.is_successful, response.status_code, response.body.text contains

rule:
  id: example-api-spring-boot-heapdump
  type: API
  alert:
    name: Spring Boot heapdump downloadable
    context: |
      A request to the actuator heapdump endpoint returned a
      successful response containing JVM heap markers, indicating a
      full memory dump is downloadable.
    severity: HIGH
    category: INFORMATION_DISCLOSURE
  seed:
  - protocol: rest
    method: GET
    path: /mappings
    headers: {}
  - protocol: rest
    method: GET
    path: /configprops
    headers: {}
  - protocol: rest
    method: GET
    path: /actuator/mappings
    headers: {}
  - protocol: rest
    method: GET
    path: /actuator/configprops
    headers: {}
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.status_code
    is: 200
  - if: response.body.text
    contains: heapdump
  - if: response.body.text
    contains: org.springframework

References:


Injection (8)

SQL injection in JSON body via JQ-targeted payloads

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:


OS command injection via shell substitution marker

Append ; echo escape-cmd-marker to every successful request body and alert when the marker appears in the response — the input was handed to a shell.

Command injection appears wherever an API passes user input to system, exec, backticks, Runtime.exec, os.system, subprocess.Popen(shell=True), or any equivalent. The deterministic fingerprint is to append a benign-looking shell chain (; echo …, && echo …) and check whether the echo output is reflected anywhere in the response.

The marker escape-cmd-marker is unique enough that any appearance in the response strongly indicates shell evaluation.

When to use: APIs that touch the filesystem, run external tools (image conversion, PDF rendering, archive extraction), or expose admin routes that obviously shell out (/diagnostics/ping, /admin/run, ...).

OWASP: A03:2021 Injection (Command) · CWE: CWE-78

Severity rationale: HIGH — direct OS command execution under the API service account. The Escape severity scale tops out at HIGH; treat confirmed RCE as a SEV-1 in your own incident process.

Features used: transform, detect, request.body.text mutator (values), response.body.text contains

rule:
  id: example-api-command-injection-marker
  type: API
  alert:
    name: OS command injection
    context: |
      A unique marker emitted by `echo escape-cmd-marker` appeared in
      the response, indicating that the API passed user input to a
      shell.
    severity: HIGH
    category: INJECTION
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.body.text
      regex: .+
    mutate:
    - key: request.body.text
      values:
      - ; echo escape-cmd-marker
      - '`echo escape-cmd-marker`'
      - $(echo escape-cmd-marker)
      - '| echo escape-cmd-marker'
  detect:
  - if: response.body.text
    contains: escape-cmd-marker

References:


HTTP header injection via CRLF in user-controlled input

Inject a CRLF sequence followed by a marker header into every string field of a successful request and alert if the response carries the marker as a real response header.

HTTP response splitting (a.k.a. CRLF injection) lets an attacker inject arbitrary response headers when user input is concatenated into a Set-Cookie, Location, or any other header. Browsers will then honor the injected header.

The deterministic fingerprint is to append \r\nX-Escape-CRLF: 1 to user-controlled strings and check whether X-Escape-CRLF appears in the response headers. A bare reflection in the body is not enough — the header has to be parsed by the HTTP client, which is exactly the impact we care about.

When to use: Any API that builds redirects, sets cookies, or otherwise constructs response headers from user input. Particularly relevant for legacy CGI / FastCGI backends.

OWASP: A03:2021 Injection (HTTP Response Splitting) · CWE: CWE-113

Severity rationale: HIGH — header injection enables session fixation, cache poisoning, and reflected XSS via injected Content-Type.

Features used: transform, detect, request.body.json mutator (jq), response.headers

rule:
  id: example-api-header-injection-crlf
  type: API
  alert:
    name: HTTP header injection via CRLF
    context: |
      A `\r\nX-Escape-CRLF: 1` suffix injected into user input was
      reflected as a real response header, indicating HTTP response
      splitting.
    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) |= . + "\r\nX-Escape-CRLF: 1"'
  detect:
  - if: response.headers
    key:
      is: X-Escape-CRLF

References:


NoSQL injection via operator object substitution

Replace string fields in a successful JSON request with $ne / $gt operator objects (Mongo-style) and alert if the response is still successful — the API forwards client-supplied operators to the database.

NoSQL backends like MongoDB accept query objects rather than SQL strings. When an API forwards user input as a value into a query document, an attacker can substitute the value with an operator object such as {"$ne": null} to match every record (effectively bypassing authentication or filtering).

The transform replaces every JSON string in the request with {"$ne": null} via JQ. If the API still returns success, it almost certainly passed the operator straight through to the database.

When to use: Any API backed by MongoDB, CouchDB, or another document store that accepts operator objects. The technique also works against PostgreSQL JSONB queries that use jsonb_path_query with user-controlled JSON.

OWASP: A03:2021 Injection (NoSQL) · CWE: CWE-943

Severity rationale: HIGH — typical impacts are authentication bypass, full collection enumeration, or arbitrary record updates.

Features used: transform, detect, request.body.json mutator (jq)

rule:
  id: example-api-nosql-injection-operator
  type: API
  alert:
    name: NoSQL operator injection accepted
    context: |
      A request body in which every string was replaced by
      `{"$ne": null}` returned a successful response. The endpoint
      forwards client-supplied operator objects to the database.
    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) |= {"$ne": null}'
  detect:
  - if: helpers.response.is_successful
    is: true

References:


Server-side template injection via arithmetic marker

Append ${7*7}{{7*7}} to every JSON string field of a successful request and alert if the response contains the rendered result 49 exactly twice — proving server-side template evaluation.

Server-side template injection (SSTI) happens when an API interpolates user input through a template engine such as Jinja2, Twig, ERB, FreeMarker, Velocity, or Smarty. Attackers can quickly progress from arithmetic markers to arbitrary code execution inside the template sandbox — and from there to the host.

The deterministic fingerprint is to inject the union of two common template syntaxes (${} and {{}}) holding the same arithmetic expression. If the response carries 4949 somewhere, at least one engine evaluated the marker.

When to use: Any API that renders user input through a server-side template: notification subjects, PDF generation, email previews, error messages with placeholders. SSTI is rare but devastating; this rule is cheap to leave on by default.

OWASP: A03:2021 Injection (SSTI) · CWE: CWE-1336

Severity rationale: HIGH — SSTI is consistently weaponized into RCE on the host within minutes (sandbox escape via class introspection). The Escape platform tops out at HIGH; treat it as RCE-equivalent in your own incident response.

Features used: transform, detect, request.body.json mutator (jq), response.body.text contains

rule:
  id: example-api-ssti-arithmetic-marker
  type: API
  alert:
    name: Server-side template injection
    context: |
      An arithmetic SSTI marker `${7*7}{{7*7}}` was rendered in the
      response body, indicating that user input is interpolated
      through a server-side template engine.
    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) |= . + "${7*7}{{7*7}}"
  detect:
  - if: response.body.text
    contains: '4949'

References:


Reflected XSS in JSON response body

Append a unique XSS marker to every JSON string field in a successful request and alert if the marker is reflected verbatim in the response body.

Many REST APIs return user input back in the response (echoes, confirmations, validation messages). When that input is included in a server-rendered HTML page elsewhere — for example a SPA that interpolates a server-side template — the API becomes a delivery vector for reflected cross-site scripting.

The deterministic test mutates every string in the JSON body to end with <script>console.log("escape")</script> and asserts the exact payload appears in the response text. A 1:1 reflection proves no escaping was applied.

When to use: Any REST or GraphQL endpoint that echoes user input in its response. The test is deliberately conservative — it only fires on a verbatim reflection, so HTML-encoded responses do not produce false positives.

OWASP: A03:2021 Injection (XSS) · CWE: CWE-79

Severity rationale: HIGH when the response feeds an HTML render path; MEDIUM when the response is consumed only by trusted automation. Pick HIGH unless you are sure of the consumer.

Features used: transform, detect, request.body.json mutator (jq), response.body.text contains

rule:
  id: example-api-reflected-xss-json-body
  type: API
  alert:
    name: Reflected XSS marker in JSON response body
    context: |
      A unique XSS marker injected into every JSON string field was
      reflected verbatim in the response body, indicating a missing
      output-encoding step on the server.
    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) |= . + "<script>console.log(\"escape\")</script>"
  detect:
  - if: response.body.text
    contains: <script>console.log("escape")</script>

References:


SQL injection via error message disclosure

Replace string parameters in successful requests with classic SQL breaking payloads and alert if the response leaks a SQL syntax / DBMS error string.

Error-based SQL injection is the cheapest deterministic test: send a payload that breaks SQL syntax (', ' OR 1=1--, ") OR (1=1, ...) and look for a DBMS error in the response body. If the error appears, the payload reached an unsafe SQL concatenation.

The transform replays every successful request body with each of five canonical payloads (so it generates five requests per successful original). The detector matches a regex covering MySQL, Postgres, MSSQL, Oracle and SQLite error fingerprints.

When to use: Any REST or GraphQL API that accepts string parameters in request.body.text. For requests that carry JSON, prefer the request.body.json jq variant in examples/api/injection/sql-injection-jq-string-fields.yaml.

OWASP: API10:2023 Unsafe Consumption of APIs (SQLi) · CWE: CWE-89

Severity rationale: HIGH — SQLi typically grants arbitrary read of the underlying database and frequently full host compromise via UDFs / xp_cmdshell.

Features used: transform, detect, request.body.text mutator (values fuzz), response.body.text regex

rule:
  id: example-api-sql-injection-error-disclosure
  type: API
  alert:
    name: SQL injection error disclosure
    context: |
      A SQL-breaking payload caused the API to leak a DBMS error
      string in the response body, indicating unsafe string
      concatenation in a backend query.
    severity: HIGH
    category: INJECTION
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.body.text
      regex: .+
    mutate:
    - key: request.body.text
      values:
      - ''' OR 1=1--'
      - ''') OR (''1''=''1'
      - 1; DROP TABLE escape_test--
      - '" OR "1"="1'
      - 1' UNION SELECT NULL--
  detect:
  - if: response.body.text
    regex: .*(syntax error|sql error|ORA-[0-9]+|mysql_fetch|psql:|sqlite3.OperationalError|SQLSTATE\[).*

References:


XXE — billion laughs payload accepted

Replace XML request bodies with a billion-laughs entity expansion payload and alert if the API processes it without rejection, proving entity processing is enabled.

XML External Entity (XXE) injection happens when the parser expands attacker-controlled DOCTYPE entities. The cheapest deterministic test is the billion-laughs DOS payload: nested entities that expand to a small number of bytes but balloon when materialized.

A robust API rejects the request outright (4xx). A vulnerable one will either return 200 with a successful parse, or 5xx with a parser stack trace. This rule fires on the 200 case, which is the strongest evidence of unsafe entity processing.

When to use: Any API that accepts XML payloads (legacy SOAP, SAML callbacks, RSS submission, OPC UA bridges). For modern JSON-only APIs this rule is a no-op trigger and adds zero overhead.

OWASP: API10:2023 Unsafe Consumption of APIs (XXE) · CWE: CWE-611

Severity rationale: HIGH — XXE typically escalates to local file disclosure and blind SSRF.

Features used: transform, detect, request.body.text mutator (value), request.headers

rule:
  id: example-api-xxe-billion-laughs
  type: API
  alert:
    name: XML external entity processing enabled
    context: |
      The API returned a successful response when the request body was
      replaced with a billion-laughs XML entity expansion payload. The
      XML parser is configured to expand attacker-controlled entities.
    severity: HIGH
    category: INJECTION
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.headers
      key:
        is: Content-Type
      value:
        regex: .*xml.*
    mutate:
    - key: request.body.text
      value: |
        <?xml version="1.0"?>
        <!DOCTYPE lolz [
          <!ENTITY lol "lol">
          <!ENTITY lol2 "&lol;&lol;&lol;&lol;">
          <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;">
        ]>
        <lolz>&lol3;</lolz>
  detect:
  - if: helpers.response.is_successful
    is: true

References:


Protocol (2)

Authentication credentials sent over plaintext HTTP

Alert when an authenticated request is observed on http:// rather than https:// — credentials are being transmitted in cleartext.

Even one cleartext-HTTP authenticated request is enough for a network-path attacker to capture the bearer token / session cookie. Modern services should refuse plain HTTP outright.

The detector combines two signals: the URL begins with http:// (case-insensitive), AND request.is_authenticated is true. Background scanner traffic on plain HTTP that is deliberately unauthenticated does not fire.

When to use: Any service expected to be HTTPS-only. Useful as a CI gate against forgotten staging URLs.

OWASP: A02:2021 Cryptographic Failures · CWE: CWE-319

Severity rationale: HIGH — plaintext credential transmission is direct, deterministic compromise on a network adversary path.

Features used: detect, schema.url regex, request.is_authenticated

rule:
  id: example-api-cleartext-http-credentials
  type: API
  alert:
    name: Authenticated request on plaintext HTTP
    context: |
      An authenticated request was observed on `http://` rather than
      `https://`. The credentials are being transmitted in
      cleartext over the network.
    severity: HIGH
    category: PROTOCOL
  detect:
  - if: schema.url
    regex: ^http://.*
  - if: request.is_authenticated
    is: true

References:


OPTIONS preflight leaks unintended HTTP methods

Alert if the response to an OPTIONS request advertises TRACE or CONNECT in Allow / Access-Control-Allow-Methods — methods that should never be enabled on a production API.

TRACE and CONNECT are diagnostic HTTP methods routinely abused for cross-site tracing and request smuggling when left enabled. Production APIs should advertise only the methods they actually serve.

The detector triggers on OPTIONS responses and flags any Allow or Access-Control-Allow-Methods value that mentions TRACE or CONNECT.

When to use: Any HTTP API. The check is essentially free — OPTIONS is almost always emitted by the framework router itself.

OWASP: A05:2021 Security Misconfiguration · CWE: CWE-16

Severity rationale: LOW — these methods rarely yield direct exploitation today, but their presence indicates a misconfigured router worth fixing.

Features used: detect, request.method, response.headers regex, logical or

rule:
  id: example-api-options-leaks-trace-connect
  type: API
  alert:
    name: OPTIONS advertises TRACE / CONNECT
    context: |
      An `OPTIONS` response advertised `TRACE` or `CONNECT` in
      `Allow` or `Access-Control-Allow-Methods`. These methods
      should never be enabled on a production API.
    severity: LOW
    category: PROTOCOL
  detect:
  - if: request.method
    is: OPTIONS
  - if: or
    or:
    - if: response.headers
      key:
        is: Allow
      value:
        regex: .*(TRACE|CONNECT).*
    - if: response.headers
      key:
        is: Access-Control-Allow-Methods
      value:
        regex: .*(TRACE|CONNECT).*

References:


Request Forgery (2)

Environment isolation — production reaches internal host

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:


SSRF via URL parameter pointing at internal target

For every successful request that has any string field, mutate each string to http://internal.example.com/probe and alert if the API made the outbound call (response now contains the probe target's body marker).

SSRF appears wherever an API takes a URL from user input (avatar import, webhook delivery, link unfurling, image proxying) and fetches it server-side. The deterministic test rewrites every string field of a successful request body to point at an internal probe URL and asserts the response body contains the probe's known marker.

The marker escape-ssrf-probe-marker is what the internal target should return in its body when reached. In a realistic setup that is your own controlled OOB endpoint (Burp Collaborator equivalent) — see examples/api/request_forgery/ssrf-blind-callback.yaml for blind detection.

When to use: Any API that takes URLs in payloads. Adjust the probe URL and the marker to your environment.

OWASP: API7:2023 SSRF · CWE: CWE-918

Severity rationale: HIGH — SSRF is reliably weaponized against cloud metadata endpoints and internal services.

Features used: transform, detect, request.body.json mutator (jq)

rule:
  id: example-api-ssrf-via-url-parameter
  type: API
  alert:
    name: SSRF via URL parameter
    context: |
      The API fetched an attacker-supplied internal URL when every
      string field of the request body was rewritten to point at
      `http://internal.example.com/probe`.
    severity: HIGH
    category: REQUEST_FORGERY
  transform:
    trigger:
    - if: helpers.response.is_successful
      is: true
    - if: request.body.json
      jq: any(.. | strings)
    mutate:
    - key: request.body.json
      jq: (.. | strings) |= "http://internal.example.com/probe"
  detect:
  - if: response.body.text
    contains: escape-ssrf-probe-marker

References:


Resource Limitation (2)

Missing rate-limit headers on authenticated route

Alert on every successful authenticated response that does not advertise any of the standard rate-limit headers — usage caps are either absent or invisible to clients.

Modern APIs surface their rate limits in headers (X-RateLimit-Limit, X-RateLimit-Remaining, RateLimit-Limit, ...). Their absence either means there is no rate limit at all, or the API silently throttles without telling clients — both are bad.

The detector OR's the most common header names so a single advertised limit is enough to satisfy the rule.

When to use: Any authenticated REST or GraphQL API. Particularly important on credential / login endpoints to deter brute-force.

OWASP: API4:2023 Unrestricted Resource Consumption · CWE: CWE-770

Severity rationale: LOW — observability finding rather than direct vulnerability; elevate to MEDIUM on credential-touching endpoints.

Features used: detect, request.is_authenticated, response.headers, logical not, logical or

rule:
  id: example-api-missing-rate-limit-header
  type: API
  alert:
    name: Missing rate-limit headers
    context: |
      An authenticated successful response did not advertise any
      `X-RateLimit-*` / `RateLimit-*` / `Retry-After` header. Usage
      caps are either absent or invisible to clients.
    severity: LOW
    category: RESOURCE_LIMITATION
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: request.is_authenticated
    is: true
  - if: not
    not:
      if: or
      or:
      - if: response.headers
        key:
          regex: (?i)x-ratelimit-.*
      - if: response.headers
        key:
          regex: (?i)ratelimit-.*
      - if: response.headers
        key:
          is: Retry-After

References:


Slow endpoint suggests unbounded query

Alert on any successful authenticated response whose duration exceeds 5 seconds — likely an unbounded list, missing pagination, or expensive join.

Latency outliers usually mean the request reached an unbounded database operation: SELECT * without LIMIT, missing pagination, recursive resolver in GraphQL, etc. They are both a DoS amplifier (cheap requests with expensive backend cost) and a UX problem.

The detector uses the response.duration_ms integer matcher with gt: 5000. Tighten or relax the threshold to your SLA.

When to use: Any API with a defined latency SLA. Particularly useful as a CI regression rule to flag list endpoints that lose pagination.

OWASP: API4:2023 Unrestricted Resource Consumption · CWE: CWE-405

Severity rationale: LOW — observability finding; aggregate severity rises with request volume on the slow endpoint.

Features used: detect, response.duration_ms

rule:
  id: example-api-slow-endpoint
  type: API
  alert:
    name: Slow endpoint
    context: |
      A successful authenticated response took more than 5 seconds,
      indicating a potentially unbounded query, a missing
      pagination cap, or an expensive join.
    severity: LOW
    category: RESOURCE_LIMITATION
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: request.is_authenticated
    is: true
  - if: response.duration_ms
    gt: 5000

References:


Sensitive Data (4)

JWT bearer token transmitted in URL query string

Alert on any request URL containing a JWT-shaped token in a query parameter — the token will be logged in proxy / CDN / browser history.

Bearer tokens belong in Authorization headers, never in URLs. URLs are persisted in:

  • reverse-proxy access logs,
  • CDN edge logs,
  • browser history,
  • Referer headers leaked to third-party scripts on the destination page.

The detector uses a regex against schema.url looking for the classic three-segment base64 JWT shape preceded by an = (any query parameter).

When to use: Any API. Particularly important to enforce in CI for any integration that historically used URL-based tokens (legacy SSO callbacks, magic-link flows).

OWASP: A02:2021 Cryptographic Failures · CWE: CWE-598

Severity rationale: MEDIUM — exposure surface is broad (logs, history) but exploitation requires reading those logs.

Features used: detect, schema.url regex

rule:
  id: example-api-jwt-in-url-query
  type: API
  alert:
    name: JWT bearer token transmitted in URL query string
    context: |
      The request URL contains a JWT-shaped token in a query
      parameter, exposing the token to proxy / CDN / browser-history
      logging.
    severity: MEDIUM
    category: SENSITIVE_DATA
  detect:
  - if: schema.url
    regex: .*[?&][A-Za-z_-]+=eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+.*

References:


Credit-card number leaked in response body

Alert when a successful response body contains a string passing a basic credit-card pattern + Luhn shape — the API is leaking payment data.

Payment card data should never leave a PCI-scoped subsystem in plaintext. Cards are usually returned as last4 only; a full-PAN response indicates a serialization bug or a forgotten development field.

The detector regex matches Visa / MasterCard / Amex / Discover PAN shapes. Luhn validation is intentionally not performed in YAML — it would require a code-side detector — so this rule may fire on synthetic test PANs (e.g. 4111111111111111); treat matches as worth investigating rather than confirmed leaks.

When to use: Any payment-adjacent API. Particularly important on internal admin endpoints that may accidentally serialize the full card object.

OWASP: API3:2023 BOPLA · CWE: CWE-359

Severity rationale: HIGH — full PAN exposure has direct compliance impact (PCI DSS).

Features used: detect, response.body.text regex

rule:
  id: example-api-credit-card-in-response
  type: API
  alert:
    name: Credit-card PAN in response body
    context: |
      The response body contains a string matching a Visa /
      MasterCard / Amex / Discover PAN shape, indicating possible
      full-card-number disclosure.
    severity: HIGH
    category: SENSITIVE_DATA
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.body.text
    regex: .*\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b.*

References:


Password hash leaked in API response

Alert when a successful response body contains a value matching a common password-hash format (bcrypt, scrypt, argon2, MD5, SHA-1) — the user object is over-serialized.

Password hashes leave the server only when the API returns the raw user model. This is almost always a bug introduced by a new developer running User.toJson() without an explicit field allow-list.

The detector regex matches the well-known prefixes for bcrypt ($2[ayb]$), scrypt ($s2$), argon2 ($argon2id$), and the canonical hex shapes for MD5 (32 hex chars) and SHA-1 (40 hex chars).

When to use: Any API that returns user objects. This rule pairs with examples/api/information_disclosure/sensitive-fields-jq.yaml for full coverage.

OWASP: API3:2023 BOPLA · CWE: CWE-200

Severity rationale: HIGH — leaked hashes are crackable offline at scale, defeating most password policies.

Features used: detect, response.body.text regex

rule:
  id: example-api-password-hash-in-response
  type: API
  alert:
    name: Password hash leaked in response body
    context: |
      The response body contains a value matching a bcrypt / argon2
      / scrypt / MD5 / SHA-1 hash format. The user object is
      over-serializing the password hash field.
    severity: HIGH
    category: SENSITIVE_DATA
  detect:
  - if: helpers.response.is_successful
    is: true
  - if: response.body.text
    regex: .*(\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}|\$argon2id?\$v=[0-9]+\$|\$s2\$[0-9]+\$).*

References:


PII (email addresses) leaked in unauthenticated response

Alert when an unauthenticated successful response contains more than one email address — the endpoint is leaking user PII to anonymous callers.

Endpoints that returned a single email belonging to the caller are usually intentional (/me, /profile). Endpoints that return multiple emails to an anonymous caller almost never are — they leak the user list (a precursor to credential stuffing).

The detector uses helpers.regex_matches.count against an email regex with gt: 1, requiring strictly more than one unique email match in the response.

When to use: Any public listing endpoint. Tune the threshold (gt: 1) up or down based on your tolerance.

OWASP: API3:2023 BOPLA · CWE: CWE-359

Severity rationale: MEDIUM — leaked email lists fuel credential stuffing and targeted phishing.

Features used: detect, request.is_authenticated, helpers.regex_matches.count

rule:
  id: example-api-pii-emails-in-response
  type: API
  alert:
    name: Multiple email addresses leaked to anonymous caller
    context: |
      An unauthenticated successful response contained more than one
      email address, indicating the endpoint is leaking user PII to
      anonymous callers.
    severity: MEDIUM
    category: SENSITIVE_DATA
  detect:
  - if: request.is_authenticated
    is: false
  - if: helpers.response.is_successful
    is: true
  - if: helpers.regex_matches.count
    regex: '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
    gt: 1

References: