API Security
Rules (for AI agents)
ALWAYS
- Require authentication on every non-public endpoint. Default to authenticated; opt
out for genuinely public routes by explicit annotation.
- Consult
auth-security for who the caller is and what they may touch. It owns
the credential (algorithm pinning, expiry, lifetime, session, CSRF) and the
permission (object-level BOLA/IDOR checks, multi-key routes, streaming subjects,
function-level role gating). Any endpoint that reads or writes a resource by id
needs it; this skill covers everything else about the endpoint.
- Validate all request inputs against an explicit schema (JSON Schema, Pydantic,
Zod, validator/v10 struct tags). Reject early; never propagate untrusted input
deeper.
- Enforce rate limits at the route level for authentication endpoints, password reset,
and any expensive operation.
- Consult
error-handling-security for what an error response may carry. It owns the
boundary between what crosses to the client and what stays in the log, including
presence-of-record disclosure (User not found vs Invalid credentials).
- Include
Cache-Control: no-store on responses containing personal or sensitive
data.
NEVER
- Use sequential integer IDs in URLs for resources accessible across tenants. Use
UUIDs or unguessable opaque IDs.
- Mass-assign request bodies directly to ORM models (
User(**request.json)) — this
enables privilege escalation when the model has admin fields the user shouldn't
control.
- Key a rate-limit / lockout counter on a client-controllable header (leftmost
X-Forwarded-For, X-Real-IP, Forwarded) — rotating it yields a fresh bucket per
request and defeats the limit. Derive the client IP from the trusted-proxy hop count
(or key on the authenticated user), and fail closed on limiter error.
- Use
HTTP GET for any state-changing operation — GET should be safe and
idempotent.
- Rely on network position (IP allowlist, VPN, private subnet, "internal
only", a WAF/edge rule) as the only control on a sensitive endpoint.
Reachability is not authentication: the moment there's an SSRF, a compromised
internal host, a tenant on the network, or a boundary change, an
unauthenticated "internal" endpoint (
permission_classes = [AllowAny],
no RequireAuth) is wide open. Enforce auth/authz at the service itself,
behind any network control.
- Place security controls (auth, field-stripping, CSRF, rate-limit, input
validation) only at a gateway / BFF / proxy while the backend service is
also directly reachable. An attacker calls the service directly and
bypasses every proxy-layer control — controls must live at the service that
owns the data. (A common variant: the gateway checks that a JWT is present
but the service never checks the caller's role or object-level ownership —
the service reads the subject id from the body/path/query and trusts it.)
KNOWN FALSE POSITIVES
- Public marketing-site endpoints serving anonymous traffic legitimately have no auth
and no rate limits beyond the load balancer.
- Sequential IDs in paths are fine for genuinely public, non-tenant-scoped resources
(e.g. blog post slugs, public product catalog items).
- Health-check endpoints (
/healthz, /ready) intentionally bypass auth.
- A network control (mTLS service mesh, NetworkPolicy, private ingress) is fine
as defense-in-depth — the anti-pattern is only when it's the sole control
and the service itself authenticates nothing.
- Mutual-TLS / SPIFFE workload identity between services is authentication
(a cryptographic caller identity), not mere network position — mTLS-authenticated
service-to-service calls are fine even on a private network.
Context (for humans)
The OWASP API Top 10 differs from the web Top 10 mostly because APIs have weaker
defaults: they often skip CSRF, they expose object IDs directly, and they tend to
trust developer-provided client-side state. This skill codifies the most common
high-impact mistakes.
A recurring architectural failure is trusting the perimeter instead of the
service: a BFF/gateway enforces auth, strips fields, and checks CSRF, while the
core service is also directly reachable and authenticates nothing because it's
"internal". Anyone who can reach the core service — via SSRF, a foothold inside
the allowlisted network, or simply a public DNS name that resolves to the same
backend — bypasses every perimeter control. Network position is a mitigation, not
an authentication boundary; the owning service must enforce auth/authz itself.
References
1---2name: api-security3description: OWASP API Top 10 for HTTP, GraphQL, and gRPC endpoints: input validation, route rate limiting, mass assignment, response caching, and gateway-versus-service control placement. Use when generating or reviewing HTTP handlers, GraphQL resolvers, gRPC service methods, or any API endpoint change.4---56# API Security78## Rules (for AI agents)910### ALWAYS11- Require authentication on every non-public endpoint. Default to authenticated; opt12 out for genuinely public routes by explicit annotation.13- Consult `auth-security` for **who the caller is and what they may touch**. It owns14 the credential (algorithm pinning, expiry, lifetime, session, CSRF) and the15 permission (object-level BOLA/IDOR checks, multi-key routes, streaming subjects,16 function-level role gating). Any endpoint that reads or writes a resource by id17 needs it; this skill covers everything else about the endpoint.18- Validate all request inputs against an explicit schema (JSON Schema, Pydantic,19 Zod, validator/v10 struct tags). Reject early; never propagate untrusted input20 deeper.21- Enforce rate limits at the route level for authentication endpoints, password reset,22 and any expensive operation.23- Consult `error-handling-security` for what an error response may carry. It owns the24 boundary between what crosses to the client and what stays in the log, including25 presence-of-record disclosure (`User not found` vs `Invalid credentials`).26- Include `Cache-Control: no-store` on responses containing personal or sensitive27 data.2829### NEVER30- Use sequential integer IDs in URLs for resources accessible across tenants. Use31 UUIDs or unguessable opaque IDs.32- Mass-assign request bodies directly to ORM models (`User(**request.json)`) — this33 enables privilege escalation when the model has admin fields the user shouldn't34 control.35- Key a rate-limit / lockout counter on a **client-controllable header** (leftmost36 `X-Forwarded-For`, `X-Real-IP`, `Forwarded`) — rotating it yields a fresh bucket per37 request and defeats the limit. Derive the client IP from the trusted-proxy hop count38 (or key on the authenticated user), and fail closed on limiter error.39- Use `HTTP GET` for any state-changing operation — GET should be safe and40 idempotent.41- Rely on **network position** (IP allowlist, VPN, private subnet, "internal42 only", a WAF/edge rule) as the *only* control on a sensitive endpoint.43 Reachability is not authentication: the moment there's an SSRF, a compromised44 internal host, a tenant on the network, or a boundary change, an45 unauthenticated "internal" endpoint (`permission_classes = [AllowAny]`,46 no `RequireAuth`) is wide open. Enforce auth/authz at the service itself,47 behind any network control.48- Place security controls (auth, field-stripping, CSRF, rate-limit, input49 validation) only at a gateway / BFF / proxy while the backend service is50 **also directly reachable**. An attacker calls the service directly and51 bypasses every proxy-layer control — controls must live at the service that52 owns the data. (A common variant: the gateway checks that a JWT is *present*53 but the service never checks the caller's *role* or *object-level ownership* —54 the service reads the subject id from the body/path/query and trusts it.)5556### KNOWN FALSE POSITIVES57- Public marketing-site endpoints serving anonymous traffic legitimately have no auth58 and no rate limits beyond the load balancer.59- Sequential IDs in paths are fine for genuinely public, non-tenant-scoped resources60 (e.g. blog post slugs, public product catalog items).61- Health-check endpoints (`/healthz`, `/ready`) intentionally bypass auth.62- A network control (mTLS service mesh, NetworkPolicy, private ingress) is fine63 as **defense-in-depth** — the anti-pattern is only when it's the *sole* control64 and the service itself authenticates nothing.65- Mutual-TLS / SPIFFE workload identity between services **is** authentication66 (a cryptographic caller identity), not mere network position — mTLS-authenticated67 service-to-service calls are fine even on a private network.6869## Context (for humans)7071The OWASP API Top 10 differs from the web Top 10 mostly because APIs have weaker72defaults: they often skip CSRF, they expose object IDs directly, and they tend to73trust developer-provided client-side state. This skill codifies the most common74high-impact mistakes.7576A recurring architectural failure is **trusting the perimeter instead of the77service**: a BFF/gateway enforces auth, strips fields, and checks CSRF, while the78core service is also directly reachable and authenticates nothing because it's79"internal". Anyone who can reach the core service — via SSRF, a foothold inside80the allowlisted network, or simply a public DNS name that resolves to the same81backend — bypasses every perimeter control. Network position is a mitigation, not82an authentication boundary; the owning service must enforce auth/authz itself.8384## References8586- `references/verifying-findings.md` — confirm or refute a finding, then lock it87- `checklists/auth_patterns.yaml`88- `checklists/input_validation.yaml`89- [OWASP API Security Top 10 2023](https://owasp.org/API-Security/editions/2023/en/0x00-introduction/).90- [RFC 9700 — OAuth 2.0 Security BCP](https://datatracker.ietf.org/doc/html/rfc9700).