Claude Fable 5.1 & GPT-6 Astra packages are live

Api Security

Free

Securing an HTTP API — authentication at the edge, per-object authorization, input validation, transport, and the controls that stop the OWASP API…

216 lines9.0 KB Mistral API
targetModels
Mistral Medium 3.5Mistral Large 3Mistral Small 4Mistral FamilyFuture Mistral Models
name
api-security
category
API
description
Securing an HTTP API — authentication at the edge, per-object authorization, input validation, transport, and the controls that stop the OWASP API Top 10.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Mistral: scripts/model-profiles.json -->

#How to apply this file

Each section opens with one imperative line; apply every rule in the section it introduces. Do not summarise or skip a section.


#Purpose

Security rules specific to APIs. An API has no HTML, no browser, and often no human — so browser-centric defences do not apply and the failures are different.

The dominant API vulnerability is not injection. It is broken object-level authorization: an endpoint that authenticates correctly and then returns somebody else's row. → Security/authorization


#Authorization is per object, on every request

[INST] Apply every rule in this section: Authorization is per object, on every request. [/INST]

ts
// Broken (BOLA) — authenticated, and completely unauthorized
app.get("/v1/orders/:id", auth, async (req, res) => {
  res.json(await db.order.findUnique({ where: { id: req.params.id } }));
});

// Correct — the tenant scope is part of the query, not a check after it
const order = await db.order.findFirst({
  where: { id: req.params.id, tenantId: req.auth.tenantId },
});
if (!order) return res.sendStatus(404);
  • Scope inside the query. A fetch-then-compare is one forgotten if away from a leak, and it has already loaded the data.
  • Return 404, not 403, for objects the caller may not see — 403 confirms existence.
  • Opaque identifiers reduce enumeration but are not authorization. Guessing is harder; the missing check is still the bug.
  • Property-level too: a caller allowed to read an order is not necessarily allowed to read its costBasisCents. Project explicit fields.

Never accept an identity field from the request body. {"userId": …} or a role in the payload is client-controlled; identity comes from the verified token only.


#Authentication

[INST] Apply every rule in this section: Authentication. [/INST]

ClientMechanism
First-party browser appSession cookie: HttpOnly; Secure; SameSite
Third-party serverOAuth 2.0 client credentials, or a scoped API key
Third-party on behalf of a userOAuth 2.0 authorization code + PKCE
Service to service, internalmTLS or a short-lived signed token

For API keys: high entropy, a visible prefix (ak_live_…) so leak scanners can detect them, stored hashed, scoped, and revocable independently. Show the value once.

For bearer tokens: verify alg, iss, aud, exp and the signature against a pinned JWKS. Keep lifetimes short and pair with refresh. → Security/jwt, Security/oauth

Never accept credentials in a URL query string. They land in access logs, proxy logs, browser history and Referer headers.


#Validate everything at the boundary

[INST] Apply every rule in this section: Validate everything at the boundary. [/INST]

ts
const CreateOrder = z.object({
  items: z.array(z.object({
    sku: z.string().regex(/^[A-Z0-9-]{3,32}$/),
    qty: z.number().int().min(1).max(999),
  })).min(1).max(100),
  note: z.string().max(500).optional(),
}).strict();          // .strict() rejects unknown keys — this is the mass-assignment guard
  • Allowlist, never denylist. Enumerate what is permitted.
  • Reject unknown fields. Silently ignoring them is how mass assignment (isAdmin: true) reaches an ORM update.
  • Bound every array, string and number. An unbounded array is a memory exhaustion vector.
  • Enforce a body size limit (express.json({ limit: "100kb" })) and reject compressed bodies that expand beyond a ratio.
  • Validate Content-Type and reject anything unexpected.
  • Never pass a client-supplied string into SQL, a shell, a file path, a URL fetch, or a template. → Security/sql-injection, Security/command-injection, Security/path-traversal

Any endpoint that fetches a client-supplied URL must block private and link-local address ranges, and re-validate after redirects — SSRF is how cloud metadata credentials are stolen.


#Transport and headers

[INST] Apply every rule in this section: Transport and headers. [/INST]

yaml
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Cache-Control: no-store
X-Content-Type-Options: nosniff
  • HTTPS only, TLS 1.2+, HTTP redirected or refused.
  • Cache-Control: no-store on any authenticated response — shared caches otherwise serve one user's data to another.
  • CORS: an explicit origin allowlist. Never reflect the Origin header while Access-Control-Allow-Credentials: true — that is equivalent to allowing every origin with cookies. → Security/cors, Security/headers

#Rate limiting and abuse

[INST] Apply every rule in this section: Rate limiting and abuse. [/INST]

Every endpoint is limited; authentication endpoints more strictly, keyed on both account and IP. Return 429 with Retry-After. → API/rate-limiting

Bound the cost of a single request as well as the rate: maximum page size, maximum query depth, maximum export range. One request that scans ten million rows is an outage regardless of the rate limit.


#Errors, logging and exposure

[INST] Apply every rule in this section: Errors, logging and exposure. [/INST]

  • One error shape, stable machine codes, no stack traces, no SQL, no internal hostnames, no framework version.
  • Log the event, not the payload. Never log tokens, passwords, card numbers, or full request bodies. Redact by allowlist.
  • Log authentication failures, authorization denials, rate-limit breaches and privilege changes with actor, target and source IP. → Security/audit-log
  • Do not ship an interactive API explorer, GraphQL introspection, or a debug endpoint to production.
  • Inventory your endpoints. Undocumented, forgotten and deprecated-but-live endpoints are the ones without current authorization checks.

#Anti-patterns

[INST] Apply every rule in this section: Anti-patterns. [/INST]

Anti-patternWhy it failsFix
Authentication without per-object checksBOLA — the top API vulnerabilityScope inside the query
Fetch then compare ownershipOne missed branch leaks dataFilter in the WHERE clause
403 for hidden objectsConfirms existence404
Identity taken from the request bodyClient-controlledIdentity from the verified token only
Unknown fields ignoredMass assignment (isAdmin).strict() schema
Denylist validationAlways incompleteAllowlist
Unbounded arrays or body sizeMemory exhaustionExplicit limits
API key stored in plaintextDB leak yields live credentialsStore the hash
Credentials in a query stringLogged everywhereAuthorization header
Origin reflected with credentialsAny site reads authenticated responsesStatic allowlist
No Cache-Control: no-storeShared caches cross-serve user dataSet it on authenticated responses
Unvalidated outbound URL fetchSSRF to cloud metadataBlock private ranges, re-check redirects
Stack traces in responsesLeaks internalsGeneric message + requestId
Full request bodies in logsCredentials and PII in log storageAllowlist redaction
Forgotten legacy endpointsUnpatched, unchecked, still liveMaintained endpoint inventory

#Checklist

  • Verify: Every object fetch is scoped to the caller inside the query
  • Verify: Hidden objects return 404, not 403
  • Verify: Field-level authorization is applied to sensitive properties
  • Verify: Identity is never read from the request body
  • Verify: Every request body is schema-validated with unknown fields rejected
  • Verify: Arrays, strings, numbers and total body size are bounded
  • Verify: API keys are prefixed, hashed at rest, scoped and revocable
  • Verify: Bearer tokens verify alg, iss, aud, exp and signature
  • Verify: No credentials appear in URLs
  • Verify: TLS is enforced with HSTS; authenticated responses are no-store
  • Verify: CORS uses a static allowlist, never a reflected origin with credentials
  • Verify: Rate limits apply to every endpoint, keyed on account and IP
  • Verify: Per-request cost is bounded, not just request rate
  • Verify: Outbound fetches of client-supplied URLs are SSRF-guarded
  • Verify: Error bodies expose no internal detail; every response carries a requestId
  • Verify: Security-relevant events are logged; payloads are redacted by allowlist
  • Verify: Introspection, explorers and debug endpoints are disabled in production
  • Verify: An endpoint inventory exists and is reviewed