Playbook library

API security baseline

Generated endpoint code is confident and structurally correct, and that is exactly the problem: it looks finished while trusting its input, returning whole database rows, and authorizing on the route instead of the record. These rules target the failures that survive a linter and a quick human read.

Written for
Teams shipping HTTP APIs — Node, Python, Go, any framework.
Not covered
Nothing here replaces a dependency scanner, a secrets scanner, or a pentest. These rules judge the code in the diff — they cannot see your gateway config, your IAM policies, or a vulnerability in a package you pulled in.

10 rules6 blocking4 warning

Start a free playbook →

01 Parse request input before using it

error

Every handler that reads a body, query string, path param or header must parse it through a schema (zod, pydantic, JSON Schema…) before the value reaches a service or a query. Passing an unvalidated object straight through is rejected, including when a type annotation claims its shape. A type annotation is a compile-time claim about runtime data you do not control — mass assignment and injection both enter through the gap between the two.

Blocks: an unvalidated boundary is exploitable the day it ships, and it is trivially visible in the diff.

Scopesrc/routes/**src/**/*.controller.*apps/api/**

Tagssecurityvalidationapi

02 Authorize on the record, not the route

error

A handler that loads a resource by id must also assert that the resource belongs to the caller — owner, tenant, or an explicit permission check — in the same query or immediately after it. Authenticated is not authorized. Route-level middleware cannot see which row you fetched, so IDOR is the flaw that survives every layer of generic protection.

Blocks: this is the most common exploitable bug in AI-written CRUD code.

Scopesrc/routes/**src/**/*.controller.*apps/api/**

Tagssecurityauthorization

03 Never serialize a raw database row

error

Responses are built from an explicit DTO or a named column selection. Returning an entity directly, or spreading one into the response object, is rejected. A `select *` plus a spread leaks whatever column someone adds next — password hashes, internal flags, soft-delete markers — and nothing in the diff that adds the column will look wrong.

Blocks: the leak is silent and retroactive — it appears without anyone editing this file.

Scopesrc/routes/**src/**/*.controller.*apps/api/**

Tagssecurityapiprivacy

04 Verify webhook signatures against the raw body

error

A webhook handler verifies the provider signature over the unparsed request body before any parsing, database write, or side effect. Body-parsing middleware must be disabled on those routes. An unverified webhook endpoint is an unauthenticated write endpoint that anyone who reads your docs can call.

Blocks: an unsigned webhook route is a direct path into your write model.

Scope**/webhooks/****/*webhook*

Tagssecurityintegrations

05 Secrets come from config, with no fallback

error

Credentials, API keys and signing secrets are read from the typed config module. A literal secret, a value committed in a fixture, or a `process.env.X || "dev-secret"` style fallback is rejected. A fallback secret becomes the production secret on the first deploy where the environment variable is missing, and nothing fails loudly enough for anyone to notice.

Blocks: a committed or defaulted secret is a disclosure the moment it merges.

Scopewhole repository

Tagssecurityconfig

06 CORS origins are an explicit allowlist

error

The allowed origins are an explicit list from config. Reflecting `req.headers.origin`, or combining a wildcard with credentials, is rejected. Reflecting the caller's origin with credentials enabled removes the same-origin policy for every authenticated route at once.

Blocks: it silently disables the browser protection the rest of the auth design assumes.

Scopesrc/**apps/api/**

Tagssecurityapi

07 Compare secrets in constant time

warning

API keys, session tokens and signatures are compared with a constant-time function (`crypto.timingSafeEqual`, `hmac.compare_digest`), never `===` or `==`. String comparison returns early on the first differing byte, so the response time leaks how much of the secret the attacker already guessed — and the fix is one function call, so there is no reason to leave the leak there.

Warns rather than blocks: the attack is real but slow and noisy, and the fix is a one-line follow-up.

Scopesrc/**/auth/**src/**/*token*src/**/*signature*

Tagssecurityauth

08 Unauthenticated routes declare a rate limit

warning

Any route reachable without a session — login, signup, password reset, public search, magic links — must attach a rate limiter or reference the gateway policy that covers it in a comment. Without one, credential stuffing and reset-email flooding cost the attacker nothing.

Warns: the limiter often lives at the gateway rather than in the diff, so this asks the reviewer to confirm it exists somewhere rather than failing the build.

Scopesrc/routes/**apps/api/**

Tagssecurityabuse

09 Errors return a code, not an internal

warning

Client-facing errors carry a stable machine-readable code and a generic message. The stack trace, the failing query and the upstream response body go to the log only. Echoing `err.message` to the client is how connection strings, SQL fragments and internal hostnames end up in a bug report screenshot.

Warns: it is an information leak, not a direct compromise — but it feeds every other attack.

Scopesrc/routes/**src/**/*.controller.*apps/api/**

Tagssecurityerrorsapi

10 Logs carry identifiers, not payloads

warning

Log an event name and the ids involved. Never log a request body, an authorization header, a token, or a full user record — including inside a catch block. Logs are shipped to third parties and retained far longer than the data policy that covers the database.

Warns: it is a compliance and retention problem rather than an immediate breach.

Scopewhole repository

Tagssecurityprivacylogging

Before you import all of it

Severity is the lever that decides whether people keep the gate on. Only error blocks a merge — bring these in as warnings first, look at what actually fires on a few weeks of real pull requests, and promote the rules that earned it. And rewrite the scope globs: the paths above are examples, and a rule only runs against files its globs match.

Rules go into Normik one at a time — through the playbook form, or by letting a coding agent propose them over MCP from an existing repository. There is no bulk markdown import: the copy button above is for your AGENTS.md, for a PR where the team argues about the list, and to keep beside you while you enter the rules you actually kept.

Every rule here is judged the way Normik judges it: on the lines the pull request adds. Nothing in this playbook depends on the PR description, on a file the diff does not touch, or on the history of a file — those are real review standards, but a gate that cannot see the evidence would either stay silent or guess.

The anatomy of a rule, how severity and globs behave, versioning and the AGENTS.md export are all in the playbook documentation.

Your agents already code fast. Make them code like your team.

Start free — no card required