Security
Skill MARUCIE/openclaw-foundry/web/public/packs/spellbook-security-auditor/skills/security
The curated AI Agent skill marketplace — 37K+ vetted skills, S/A/B/C ratings, deploy anywhere
npx -y skills add MARUCIE/openclaw-foundry --skill securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when implementing authentication, authorization, input validation, or secrets management — or when auditing an existing service against OWASP Top 10 risks, configuring security headers, or running a STRIDE threat model.
SKILL.md
28.2 KB, as published. Nobody here has run it
是什么
这是一份后端安全综合规范,覆盖鉴权、授权、输入校验、密钥管理、OWASP(开放式 Web 应用安全项目)Top 10 风险、STRIDE(六类威胁建模方法)威胁建模、安全响应头等核心控制点,让团队从写代码起就具备防护意识,而不是上线后再补救。
怎么用
- 新功能立项时按 STRIDE 框架做威胁建模,把潜在攻击面在设计阶段就列清楚。
- 写鉴权和会话管理代码时套用文档的标准模板,密码哈希、Token 签发、退出失效一次到位。
- 处理用户输入时按白名单校验、参数化查询、输出编码三件套防御,杜绝注入和 XSS(跨站脚本攻击)。
- 上线前对照 OWASP Top 10 清单逐项自检,特别是访问控制、安全配置、依赖漏洞三大重灾区。
- 配置 Content-Security-Policy(内容安全策略)等安全响应头时按文档基线套用,逐步收紧不一刀切。
架构图
flowchart LR
A[功能设计] --> B[STRIDE 建模]
B --> C[安全编码]
C --> D[OWASP 自检]
D --> E[依赖扫描]
E --> F[上线放行]
Security
A comprehensive reference for securing backend APIs and web applications — covering authentication, authorization, secrets, input validation, security headers, threat modeling, and dependency hygiene across Python, TypeScript, and Go.
When to Activate
- Implementing authentication or session management
- Reviewing code for security vulnerabilities or injection risks
- Handling secrets, API keys, or credentials in code or config
- Designing authorization — who can access what
- Configuring security headers for an API or web application
- Setting up dependency scanning in CI
- Running a threat model for a new feature or system
OWASP Top 10 Quick Reference
| # | Vulnerability | Description | Primary Mitigation |
|---|---|---|---|
| A01 | Broken Access Control | Users act outside intended permissions | Enforce ownership checks server-side on every request |
| A02 | Cryptographic Failures | Sensitive data exposed due to weak/absent encryption | TLS everywhere, strong hashing, encrypted secrets at rest |
| A03 | Injection | Hostile data sent to an interpreter | Parameterized queries, input validation, output encoding |
| A04 | Insecure Design | Missing or ineffective security controls in the design | Threat modeling, secure design patterns, abuse case review |
| A05 | Security Misconfiguration | Insecure defaults, open cloud storage, verbose errors | Harden configs, disable defaults, suppress stack traces in prod |
| A06 | Vulnerable Components | Using components with known vulnerabilities | Dependency scanning in CI, automated updates via Renovate/Dependabot |
| A07 | ID & Auth Failures | Weak authentication, credential stuffing, broken session management | MFA, secure password hashing, session invalidation, rate limiting |
| A08 | Software/Data Integrity Failures | Unsigned updates, insecure deserialization, CI/CD tampering | Verify supply chain, sign artifacts, restrict pipeline permissions |
| A09 | Logging & Monitoring Failures | Insufficient logging to detect breaches | Structured audit logs, alerting on anomalies, log retention policy |
| A10 | SSRF | Server fetches attacker-controlled URL | Allowlist destinations, block internal address ranges (169.254.x.x, 10.x.x.x) |
Injection (A03) — Deep Dive
Never construct queries or commands by concatenating user input.
SQL Injection
# BAD — string concatenation opens SQLi
query = f"SELECT * FROM users WHERE email = '{user_input}'"
cursor.execute(query)
# GOOD — parameterized query
cursor.execute("SELECT * FROM users WHERE email = %s", (user_input,))
// BAD
const rows = await db.query(`SELECT * FROM users WHERE email = '${userInput}'`);
// GOOD
const rows = await db.query("SELECT * FROM users WHERE email = $1", [userInput]);
// BAD
row := db.QueryRow("SELECT * FROM users WHERE email = '" + userInput + "'")
// GOOD
row := db.QueryRow("SELECT * FROM users WHERE email = ?", userInput)
Command Injection
# BAD — shell=True with user input is always dangerous
import subprocess
subprocess.run(f"convert {filename}", shell=True)
# GOOD — pass args as a list, shell=False (default)
subprocess.run(["convert", filename])
// BAD
exec(`convert ${filename}`);
// GOOD — use execFile with an array of arguments
import { execFile } from "child_process";
execFile("convert", [filename]);
// BAD
cmd := exec.Command("sh", "-c", "convert "+filename)
// GOOD
cmd := exec.Command("convert", filename)
Broken Access Control (A01) — Deep Dive
Check ownership at the handler level, not only role membership. A role check alone prevents vertical privilege escalation (a regular user doing admin things) but not horizontal privilege escalation (a user accessing another user's resources — IDOR).
IDOR (Insecure Direct Object Reference)
# BAD — checks auth, but not ownership
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user=Depends(get_current_user)):
return db.query(Invoice).filter(Invoice.id == invoice_id).first()
# GOOD — verifies the resource belongs to the caller
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user=Depends(get_current_user)):
invoice = db.query(Invoice).filter(
Invoice.id == invoice_id,
Invoice.owner_id == current_user.id, # ownership check
).first()
if not invoice:
raise HTTPException(status_code=404)
return invoice
| Escalation Type | Description | Example |
|---|---|---|
| Vertical | Lower-privileged user accesses higher-privileged functions | Regular user calls admin endpoint |
| Horizontal | User accesses another user's data at the same privilege level | User A reads User B's invoices via IDOR |
Security Misconfiguration (A05) — Deep Dive
- Remove default credentials immediately (database root passwords, admin/admin panels).
- Suppress stack traces in production — return generic error messages, log details server-side.
- Audit S3 bucket ACLs:
aws s3api get-bucket-acl --bucket <name>— buckets must never bepublic-readorpublic-read-writeunless serving static public assets intentionally. - CORS must allowlist specific origins;
Access-Control-Allow-Origin: *disables same-origin protection for all browsers.
Authentication Patterns
Password Hashing
Never store plaintext passwords or hashes produced by MD5, SHA-1, or unsalted SHA-256. Use a slow, adaptive hashing algorithm: bcrypt (work factor ≥ 12) or argon2id.
# Python — bcrypt via passlib
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto", bcrypt__rounds=12)
hashed = pwd_context.hash("user_plaintext_password")
valid = pwd_context.verify("user_plaintext_password", hashed)
// TypeScript — bcryptjs
import bcrypt from "bcryptjs";
const hashed = await bcrypt.hash(plaintext, 12); // work factor 12
const valid = await bcrypt.compare(plaintext, hashed);
// Go — golang.org/x/crypto/bcrypt
import "golang.org/x/crypto/bcrypt"
hashed, err := bcrypt.GenerateFromPassword([]byte(plaintext), 12)
err = bcrypt.CompareHashAndPassword(hashed, []byte(plaintext))
JWT (JSON Web Tokens)
Structure: Base64URL(header).Base64URL(payload).signature
Signing algorithm choice:
| Algorithm | Key Type | Use Case |
|---|---|---|
| RS256 | RSA key pair (asymmetric) | Microservices — verify without sharing secret; preferred |
| HS256 | Shared secret (symmetric) | Single-service — simpler, but secret must be shared to verify |
| ES256 | ECDSA key pair (asymmetric) | Same as RS256 but smaller tokens |
Required claims: iss (issuer), sub (subject/user ID), aud (audience), exp (expiry unix timestamp), iat (issued-at unix timestamp).
Token storage:
<!-- BAD — localStorage is readable by any JavaScript on the page (XSS risk) -->
localStorage.setItem("access_token", token);
<!-- GOOD — httpOnly cookie is inaccessible to JavaScript -->
<!-- Set by server: Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict -->
Refresh token rotation: Issue a short-lived access token (15 min) alongside a long-lived refresh token (7 days). On each refresh, invalidate the old refresh token and issue a new one. Detect token reuse — if a refresh token is used twice, revoke the entire family.
JWT verification — all 3 languages:
# Python — PyJWT
import jwt
payload = jwt.decode(
token,
public_key, # RS256: public key; HS256: shared secret
algorithms=["RS256"],
audience="my-api",
options={"require": ["exp", "iss", "sub", "aud"]},
)
// TypeScript — jose
import { jwtVerify } from "jose";
const { payload } = await jwtVerify(token, publicKey, {
issuer: "https://auth.example.com",
audience: "my-api",
});
// Go — golang-jwt/jwt
import "github.com/golang-jwt/jwt/v5"
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return publicKey, nil
}, jwt.WithAudience("my-api"), jwt.WithIssuer("https://auth.example.com"))
Session-Based Auth
For server-rendered applications, server-side sessions (stored in Redis or a database) are a simpler and often safer alternative to JWTs.
Set all three protective cookie flags:
Set-Cookie: session_id=<opaque_id>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
| Flag | Effect |
|---|---|
HttpOnly | Cookie inaccessible to JavaScript — blocks XSS token theft |
Secure | Cookie sent only over HTTPS |
SameSite=Strict | Cookie not sent on cross-site requests — blocks CSRF |
OAuth2 / OIDC
Flow Selection
| Flow | Use Case | PKCE Required |
|---|---|---|
| Authorization Code + PKCE | Browser SPA, mobile/native apps | Yes |
| Client Credentials | Machine-to-machine (M2M), backend services | No — uses client secret |
| Device Code | CLI tools, smart TV apps | No — polling-based |
Authorization Code + PKCE Steps
- App generates a cryptographically random
code_verifier(43–128 chars). - App computes
code_challenge = BASE64URL(SHA256(code_verifier)). - App redirects user to IdP authorization URL with
code_challengeandcode_challenge_method=S256. - User authenticates at IdP; IdP redirects back with
authorization_code. - App exchanges
authorization_code+code_verifier(not the challenge) at token endpoint. - IdP verifies
SHA256(code_verifier) == code_challengestored from step 3, then returns tokens. - App stores access token in
httpOnlycookie; never inlocalStorage.
Build vs Buy
Use a third-party IdP (Auth0, Clerk, Cognito, Okta) unless you have exceptional requirements. Rolling your own OAuth2/OIDC server is a multi-month project with serious security risk surface: token endpoint, PKCE, refresh rotation, MFA, brute-force protection, RBAC, and audit logging all need to be correct simultaneously.
Scope Design
# BAD — wildcard and coarse scopes leak excessive access
scopes: ["admin", "*", "readwrite"]
# GOOD — narrow, resource-specific scopes
scopes: ["read:invoices", "write:invoices", "read:profile"]
Never grant admin or wildcard scopes to third-party integrations. Use the principle of least privilege — request only the scopes needed for the operation.
Authorization Patterns
RBAC vs ABAC
| Dimension | RBAC | ABAC |
|---|---|---|
| Definition | Permissions assigned to roles; users assigned to roles | Permissions derived from attributes of user, resource, environment |
| Complexity | Low — simple to implement and audit | High — policy engine required (OPA, Casbin) |
| Flexibility | Low — coarse-grained; role explosion at scale | High — fine-grained contextual decisions |
| Best For | Small teams, well-defined job functions | Multi-tenant SaaS, dynamic access policies, data-level isolation |
Resource-Level Authorization
Check ownership in the handler, not only in middleware. Middleware can verify the token is valid and extract the role — it cannot verify the requested resource belongs to the caller.
// BAD — only checks role, not resource ownership
router.delete("/posts/:id", requireRole("user"), async (req, res) => {
await db.posts.delete({ where: { id: req.params.id } });
res.sendStatus(204);
});
// GOOD — verifies caller owns the resource before deleting
router.delete("/posts/:id", requireRole("user"), async (req, res) => {
const post = await db.posts.findUnique({ where: { id: req.params.id } });
if (!post) return res.sendStatus(404);
if (post.authorId !== req.user.id) return res.sendStatus(403); // ownership check
await db.posts.delete({ where: { id: req.params.id } });
res.sendStatus(204);
});
Policy Object Pattern
For complex logic, encapsulate authorization decisions in a policy object rather than scattering if checks across handlers.
class PostPolicy:
def can_edit(self, user, post) -> bool:
return post.author_id == user.id or user.role == "admin"
def can_delete(self, user, post) -> bool:
return post.author_id == user.id or user.role == "admin"
def can_publish(self, user, post) -> bool:
return user.role in ("editor", "admin")
# Handler
policy = PostPolicy()
if not policy.can_edit(current_user, post):
raise PermissionError("Forbidden")
Common Authorization Mistakes
- Checking permissions only in the UI — API endpoints are directly reachable; always enforce server-side.
- Enforcing access control on reads but not on writes — verify on every create, update, and delete.
- Relying on obscurity — never assume an endpoint is safe because it is undocumented.
- Missing re-authorization after role changes — invalidate sessions or tokens when a user's role is downgraded.
Secrets Management
The rule: never commit secrets. A .env file is for local development only — add it to .gitignore and never commit it to version control.
# BAD — hardcoded secret in source code
SECRET_KEY = "sk_live_abc123supersecretkey"
DATABASE_URL = "postgres://admin:password123@prod-db/app"
# GOOD — read from environment at runtime
import os
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
Secrets Progression by Environment
| Environment | Secret Storage | Notes |
|---|---|---|
| Local Dev | .env file (gitignored) | Never commit; use .env.example with fake values for documentation |
| CI (GitHub Actions) | Repository or org Secrets (${{ secrets.NAME }}) | Masked in logs; scoped to workflows |
| CI (GitLab) | CI/CD Variables (masked, protected) | Mark variables as masked to prevent log exposure |
| Production | HashiCorp Vault / AWS Secrets Manager / GCP Secret Manager / Doppler | Dynamic secrets, audit trail, automatic rotation |
Secret Rotation
- Never reuse secrets across services or environments.
- Rotate immediately on suspected exposure — treat exposure as confirmed until proven otherwise.
- Automate rotation where the provider supports it (AWS RDS credentials via Secrets Manager, Vault dynamic secrets).
- After rotation, verify all consumers are updated before invalidating the old secret.
Required .gitignore Entries
.env
.env.*
!.env.example
*.pem
*.key
*.p12
credentials.json
serviceAccountKey.json
.aws/credentials
kubeconfig
terraform.tfvars
Input Validation and Injection Prevention
Parameterized Queries
Always use parameterized queries or an ORM with parameter binding. No exceptions for "safe" input — the validation layer can be bypassed.
# BAD
cursor.execute("INSERT INTO orders (user_id, item) VALUES ('" + user_id + "', '" + item + "')")
# GOOD — psycopg2 / SQLAlchemy
cursor.execute("INSERT INTO orders (user_id, item) VALUES (%s, %s)", (user_id, item))
// BAD
await pool.query(`INSERT INTO orders (user_id, item) VALUES ('${userId}', '${item}')`);
// GOOD — pg / Prisma / Drizzle
await pool.query("INSERT INTO orders (user_id, item) VALUES ($1, $2)", [userId, item]);
// BAD
db.Exec("INSERT INTO orders (user_id, item) VALUES ('" + userID + "', '" + item + "')")
// GOOD
db.Exec("INSERT INTO orders (user_id, item) VALUES (?, ?)", userID, item)
File Upload Validation
import magic # python-magic (libmagic binding)
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE_BYTES = 10 * 1024 * 1024 # 10 MB
def validate_upload(file_bytes: bytes, claimed_extension: str) -> None:
if len(file_bytes) > MAX_SIZE_BYTES:
raise ValueError("File too large")
detected_mime = magic.from_buffer(file_bytes, mime=True)
if detected_mime not in ALLOWED_TYPES:
raise ValueError(f"File type not allowed: {detected_mime}")
# Store to a path outside the web root — never serve raw uploads directly
Key rules:
- Detect MIME type from file content (magic bytes), not from the file extension or
Content-Typeheader. - Enforce a maximum size before reading the full payload into memory.
- Store uploaded files outside the web root or in object storage, never in a publicly served directory.
- Rename files on storage — never preserve the user-supplied filename.
HTML Output Escaping
// BAD — executes arbitrary JS if userInput contains <script>...</script>
element.innerHTML = userInput;
// GOOD — use textContent for plain text
element.textContent = userInput;
// GOOD — for rich HTML, use a sanitizer library
import DOMPurify from "dompurify";
element.innerHTML = DOMPurify.sanitize(userInput);
Use auto-escaping templating engines (Jinja2, Go html/template, React JSX) — they escape by default. Only bypass escaping intentionally with | safe / dangerouslySetInnerHTML and only on content you fully control.
Validation Libraries
| Language | Library | Notes |
|---|---|---|
| Python | pydantic | Schema-first; validates at parse time; ideal for FastAPI |
| TypeScript | zod | Runtime schema validation; pairs well with tRPC |
| Go | go-playground/validator | Struct tag–based validation; widely used with Gin/Echo |
Validate at the API boundary — reject invalid input before it reaches business logic or the database.
Security Headers
Set these headers on every HTTP response from your API or web server.
| Header | Recommended Value | What It Prevents |
|---|---|---|
Content-Security-Policy | default-src 'self'; script-src 'self'; object-src 'none' | XSS, data injection, resource hijacking |
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | SSL stripping, MITM downgrade attacks |
X-Frame-Options | DENY | Clickjacking via iframe embedding |
X-Content-Type-Options | nosniff | MIME-type sniffing attacks |
Referrer-Policy | strict-origin-when-cross-origin | Referrer URL leakage to third parties |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Unauthorized browser feature access |
Cache-Control | no-store (for authenticated endpoints) | Sensitive data cached in shared proxies |
CORS configuration:
# BAD — allows any origin to make credentialed requests
CORS(app, origins="*", supports_credentials=True)
# GOOD — explicit allowlist; never wildcard for authenticated routes
CORS(app, origins=["https://app.example.com", "https://admin.example.com"])
Access-Control-Allow-Origin: * is acceptable only for fully public, unauthenticated endpoints (e.g., a public API with no user data). Never combine * with Access-Control-Allow-Credentials: true — browsers reject it, and it would be a severe security flaw if they did not.
Adding Headers in Each Language
# FastAPI — middleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
// Express — use helmet
import helmet from "helmet";
app.use(helmet()); // sets all recommended headers with sane defaults
// Go — net/http middleware
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
STRIDE Threat Modeling
STRIDE Reference Table
| Threat | Description | Mitigation Category |
|---|---|---|
| Spoofing | Claiming an identity you do not own | Authentication — verify identity on every request |
| Tampering | Modifying data in transit or at rest without authorization | Integrity — TLS, HMAC signing, database audit logs |
| Repudiation | Denying that an action occurred | Audit logging — immutable, attributable event records |
| Information Disclosure | Exposing sensitive data to unauthorized parties | Encryption at rest and in transit, access control, minimal data exposure |
| Denial of Service | Making the system unavailable to legitimate users | Rate limiting, resource quotas, circuit breakers, auto-scaling |
| Elevation of Privilege | Gaining capabilities beyond what is authorized | Authorization checks on every operation, principle of least privilege |
Threat Modeling Process
- Draw a data flow diagram (DFD). Identify every external entity, process, data store, and data flow in the feature.
- Mark trust boundaries. A trust boundary is crossed wherever data moves between different trust levels — browser to API, API to database, service to third-party, internal service to internal service.
- Apply STRIDE to each data flow and process. For each element, ask: can it be Spoofed? Tampered? Repudiated? Disclosed? Denied? Escalated?
- List mitigations. For each identified threat, record the control that addresses it (or note that it is accepted risk with justification).
- Re-review when the design changes. A threat model is not a one-time artifact — revisit it when auth, data flows, or trust boundaries change.
Lightweight threat model template:
| Data Flow | STRIDE Threats Identified | Mitigation |
|---|---|---|
| Browser → API (login) | Spoofing (credential stuffing), DoS (brute force) | Rate limit login endpoint, MFA, account lockout |
| API → Database | Injection (A03), Information Disclosure | Parameterized queries, least-privilege DB user |
| API → Third-party payment service | Tampering (webhook), Repudiation | Verify webhook HMAC signature, log all events |
| S3 presigned URL → Browser | Information Disclosure | Short-lived URLs (15 min), bucket policy denies public access |
Dependency Scanning
Tools by Language
| Language | Tool | Purpose |
|---|---|---|
| Python | pip-audit | Scans installed packages against OSV/PyPI advisory database |
| Python | safety | CVE scanning; integrates with CI |
| Python | Dependabot | Automated PRs for vulnerable package updates |
| TypeScript | npm audit | Built-in; checks against npm advisory database |
| TypeScript | socket.dev | Supply chain analysis — detects typosquatting and malicious packages |
| TypeScript | Snyk | CVE scanning with fix PRs |
| Go | govulncheck | Official Go vulnerability scanner from the Go team; checks call graph |
CI Integration
# GitHub Actions — Python
- name: Scan dependencies
run: pip-audit --strict --vulnerability-service osv
# GitHub Actions — TypeScript
- name: Scan dependencies
run: npm audit --audit-level=high
# GitHub Actions — Go
- name: Scan dependencies
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
Fail the CI build on HIGH or CRITICAL severity findings. Treat a new HIGH CVE in a direct dependency the same as a failing test — it blocks the merge.
Automated Dependency Updates
Configure Dependabot (/.github/dependabot.yml) or Renovate to open automated PRs when dependency updates are available. Keep the update cadence short (weekly) to avoid large, risky batch upgrades.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
SBOM Generation
Generate a Software Bill of Materials for auditing and compliance.
# Python
pip install cyclonedx-bom
cyclonedx-py environment -o sbom.json
# TypeScript
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Go
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest
cyclonedx-gomod app -output sbom.json
Red Flags
- JWT validated on signature only, not expiry or audience — a valid signature on an expired or wrong-audience token still passes; always check
exp,nbf, andaudclaims - RBAC without object-level ownership check — checking role but not resource ownership lets any
admin-role user access other users' data; always verifyresource.owner_id == current_user.id - Input validation inside business logic — validate at the request boundary (controller/handler); by the time data reaches domain logic it should already be trusted and clean
- STRIDE skipped for "small" features — new auth paths, file uploads, and webhooks introduce entire attack classes; STRIDE at design time costs minutes, post-incident costs days
- Dependency scanning only at release — new CVEs are published daily; run dep scanning on every PR and on a nightly schedule against the default branch
- Cookie flags missing on session tokens — without
HttpOnly,Secure, andSameSite=Strict, session cookies are vulnerable to XSS theft and CSRF; set all three - Auth middleware applied to route groups but not verified per handler — a misconfigured route group silently bypasses middleware; verify auth and ownership explicitly in each sensitive handler
Checklist
- Passwords hashed with bcrypt (factor 12+) or argon2id — never MD5/SHA1/plaintext
- JWT signed with RS256 (asymmetric) or HS256 with strong secret (32+ random bytes)
- Access tokens short-lived (15 min max); refresh tokens rotate on every use
- Secrets stored in vault/secrets manager in production — never in env files committed to git
-
.gitignorecovers.env,*.pem,*.key,credentials.json,serviceAccountKey.json - All SQL queries use parameterized statements — no string concatenation with user input
- User input validated at the API boundary with pydantic / zod / go-playground/validator
- Authorization checks resource ownership (
resource.owner_id == current_user.id), not just role - Authorization enforced on create, update, and delete — not only on read
- Security headers set on all API and web responses (CSP, HSTS, X-Frame-Options, nosniff)
- CORS configured to allowlist specific origins — no wildcard
*on authenticated routes - Dependency scanner runs in CI and fails the build on HIGH/CRITICAL CVEs
- No stack traces or internal error details exposed in production API responses
- File uploads validated by MIME type (magic bytes), size limit enforced, stored outside web root
- Threat model reviewed for any new feature touching authentication, authorization, or sensitive data