Skip to content
← Writing

Catching the Boring API Security Holes Before Someone Else Does

What apicheck looks for, why it uses two identities to test object-level access, why it refuses to be destructive, and how to run it as a CI gate with SARIF output.

· Security, TypeScript, OWASP, CI/CD

Most API security problems in business apps are not clever. A list endpoint that answers without a token. A user who can read someone else's record by changing an ID in the URL. A response that includes a field nobody meant to send.

They're boring, and they're exactly the kind of thing that slips through review because everyone assumes someone else checked. apicheck is a small command-line tool that checks them for you, on APIs you own.

How it works

Give it a base URL and an OpenAPI spec. It discovers every endpoint from the spec and runs a set of independent checks against each one.

apicheck scan https://staging.example.com \
  --spec ./openapi.json \
  --token-a "$TOKEN_A" \
  --token-b "$TOKEN_B" \
  --i-am-authorized

Every finding is mapped to the OWASP API Security Top 10, so a result is easy to explain to someone who wasn't in the room.

What it checks

Per endpoint:

  • security-headers (API8): missing HSTS, nosniff, frame protection or cache-control
  • missing-auth (API2, API5): endpoints that should need auth returning data without it
  • rate-limit (API4): no throttling under a small, hard-capped burst
  • idor (API1): identity B reading identity A's object
  • excessive-data (API3): fields like password, secret or token in responses
  • cors (API8): reflective or wildcard CORS, worst when credentials are allowed
  • cookie-flags (API8): Set-Cookie without Secure, HttpOnly or SameSite
  • info-disclosure (API8): version-leaking headers and stack traces
  • bfla (API5): a lower-privilege identity reaching privileged functions

Once per scan:

  • shadow-endpoints (API9): undocumented paths that still respond
  • inventory-drift (API9): deprecated operations still published, and version sprawl

That's 11 checks across 6 of the OWASP API Top 10.

Two identities make access bugs testable

Broken object-level authorisation sits at the top of the OWASP API Top 10, and you can't find it with one user. apicheck takes two tokens. It reads an object as identity A, then tries the same request as identity B. If B gets A's data, that's a finding:

✖ GET /users/{id}   token B read token A's resource at /users/alice (identical response body) — broken object level authorization (API1:2023) [idor]

Non-destructive on purpose

A scanner you're nervous to run is a scanner nobody runs. So apicheck observes and reports. It never exploits, modifies or exfiltrates anything.

  • A scan won't start without --i-am-authorized. Only scan systems you own or are allowed to test.
  • The rate-limit check sends a bounded burst (20 requests by default, capped at 100).
  • Concurrency defaults to 8 requests and is capped at 20.

Running it in CI

The exit code is 1 when any finding meets --fail-on (default fail), and 2 for config or spec errors. That's enough to fail a pipeline.

--sarif writes SARIF 2.1.0, which GitHub code scanning understands. Findings then show up in the Security tab and as annotations on pull requests:

- run: apicheck scan "$STAGING_URL" --spec openapi.json
       --token-a "$TOKEN_A" --token-b "$TOKEN_B"
       --i-am-authorized --sarif > apicheck.sarif
  continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: apicheck.sarif

Living with known findings

Some findings are deliberate. Your health check may be unlimited on purpose, or CORS may be handled at the gateway. Put those in .apicheckignore so the gate stays green and new problems still stand out:

rate-limit:GET /health     # health is intentionally unlimited
cors:*                     # CORS handled at the gateway

Trying it

It's not on npm yet, so build it from source (Node.js 20+):

npm install && npm run build && npm link

Source and docs: hoysengleang/api-check.