Appearance
Secure-Coding Checklist (cross-stack)
A framework-agnostic checklist for any web/app/API project (Laravel, Node/Next, Python, React Native). Derived from the Laravel Boost security.md rules and hardened with real findings from the autonomous-booking audit. Use it as a pre-merge / pre-deploy gate.
How to use: skim the headers during code review. Each item lists the failure mode, then "Do this". The starred items (*) are the ones most teams actually get wrong.
1. Authorization and object ownership (*)
The single most common real vulnerability: an endpoint trusts an ID from the request and never checks the caller owns that record (IDOR / broken object-level authorization).
- Every read or mutation keyed on a client-supplied ID must filter by the owner, e.g.
where('user_id', currentUser.id)or an explicit policy/gate check. Do not rely on the ID being unguessable. - Never trust an ID held in client state (Livewire public property, hidden form field, JWT-decoded-then-ignored, Redux store) as proof of ownership. In Livewire specifically, lock model-bound properties (
#[Locked]). - Prefer non-sequential public identifiers (UUID / nanoid) for anything reachable by URL, so records are not enumerable. Treat this as defense in depth, not the access control itself.
- Authorize the action, not just the route. A logged-in user reaching a route is not authorization to act on someone else's object.
2. Business-logic and money paths (*)
Bugs here cost real money and are invisible to scanners.
- Prices, discounts, quantities, balances, and statuses come from the server / database, never from the request. Re-derive them server-side even if the client sends them.
- Any multi-step financial mutation (charge + credit ledger + decrement stock) runs in one DB transaction.
- Enforce limits atomically. A non-locking
count()theninsert()is a TOCTOU race: two concurrent requests both pass the check. UselockForUpdate/SELECT ... FOR UPDATE, a unique constraint, or an atomic decrement. Back every "max N redemptions / one per user" rule with a DB unique constraint, not just app code. - Make every redemption/credit path go through the same guarded code. Watch for "free" shortcuts (100% off, zero-card-amount, full-store-credit) that skip the normal limit checks.
- Capacity / inventory checks must happen inside the lock that decrements them, or you overbook.
3. Webhooks and idempotency
- Verify the provider signature on every webhook (Stripe
STRIPE_WEBHOOK_SECRET, etc.). An unsigned webhook endpoint is an unauthenticated write. - Make handlers idempotent: persist the provider event ID and short-circuit on replay. Do not rely on derived record state. Side effects (emails, credits) must fire at most once.
4. Secrets management (*)
- Never commit real secrets. No live keys, tokens, or private keys in the repo, including as fallback defaults:
env('STRIPE_SECRET', 'sk_test_realkey...')leaks the key to anyone with repo access. Useenv('STRIPE_SECRET')with no default so a missing var fails fast. - Read secrets through your config layer (
config('services.x')), notenv()scattered through code. - A committed
.env.testing/ CI key must be distinct from production. Reusing an app encryption key (APP_KEY, JWT secret, session secret) between test and prod is critical: it lets an attacker forge signed URLs, decrypt cookies/sessions, and forge framework payloads. .gitignoreall.env*except.env.example(placeholders only). Rotate anything that ever landed in git history.
5. Mass assignment / over-posting
- Whitelist writable fields explicitly (
$fillable, DTOs, validated payloads, serializers). Never bind a raw request body to a model. - Globally disabling the guard (
Model::unguard(),strict: false) removes the safety net for every future write. If you must, ensure no write ever receives an unfiltered request array. - Sensitive columns (role, is_admin, balance, owner_id, price, verified_at, status) must never be in a user-writable set.
6. Injection
- Parameterize all queries. Never interpolate user input into SQL, even in "raw" expressions (
whereRaw('LOWER(name) = ?', [$name]), not"... = '$name'"). - No dynamic column/table/
orderByfrom raw user input. Map user input to an allowlist of known-safe values. - No shell exec with interpolated input. Avoid
exec/system/Processon user data; if unavoidable, pass args as an array, never a concatenated string. - No
unserialize()/ native deserialization of untrusted data. Use JSON.
7. Output / XSS
- Default to auto-escaped output (
, React's default text rendering). Only emit raw HTML ({!! !!},dangerouslySetInnerHTML,v-html) for content you sanitized through a real HTML sanitizer. nl2br()and similar do NOT escape. Escape first, then format:nl2br(e($value)).- Admin-authored content is still a stored-XSS vector against end users. Sanitize it too; "trusted admin" is not a control once a content role or a compromised admin exists.
- Set security headers: a Content-Security-Policy,
X-Content-Type-Options: nosniff,X-Frame-Options/frame-ancestors, and HSTS. Most frameworks ship none by default.
8. Authentication
- Throttle login, register, password-reset, and token endpoints (e.g. 5/min per IP+identifier) with lockout.
- Regenerate the session on login (fixation) and invalidate it on logout.
- Generic responses on login and password-reset to avoid user enumeration. Registration's "email already taken" is a common enumeration leak; decide if it matters for your threat model.
- Enforce a real password policy (min length plus a breached-password check beats complexity rules).
- If you implement email verification, actually gate sensitive routes on it; an unused
verifiedmiddleware is decoration.
9. File uploads
- Validate real MIME type and size, not just the client-sent extension/filename. Never trust the client filename.
- Store with a server-generated name on a private disk by default; serve through an authorized route.
- Block or sanitize SVG (it can carry script) and anything served inline from a public bucket.
10. APIs
- Rate-limit every authenticated and public API route, even machine-to-machine ones. A leaked client credential with no throttle is a flood / data-exhaustion vector.
- Return only the fields the caller needs (use a resource/serializer with explicit fields or a
$hiddenlist). Do not echo whole models. - Scope API queries to the authenticated client/tenant exactly like web routes.
11. Dependencies and CI
- Run a dependency audit in CI (
composer audit,npm audit --production,pip-audit) and fail on high severity. - Keep static analysis (PHPStan/Larastan, tsc/eslint, mypy) and the formatter green on every PR.
- Encrypt sensitive columns at rest (API keys, tokens) with an
encryptedcast and mark them hidden.
Quick pre-deploy gate
- No client-supplied ID acts on a record without an ownership/policy check. (#1)
- No price/limit/balance trusted from the request; limits enforced atomically + DB constraint. (#2)
- Webhooks signature-verified and idempotent. (#3)
- No real secret in the repo or as a config default; prod keys distinct from test. (#4)
- No raw request body bound to a model; no
unguard()on user-facing writes. (#5) - Queries parameterized; raw HTML only on sanitized content. (#6, #7)
- Auth + API routes throttled; security headers set. (#8, #10, #7)
composer/npm/pip auditclean in CI. (#11)