Secure Development Principles
Cross-tool AGENTS.md baseline for secure development practices across any stack.
Install path
Use this file for each supported tool in your project.
- Cursor: Save as
AGENTS.mdin your project atAGENTS.md. - Claude Code: Save as
AGENTS.mdin your project atAGENTS.md. - OpenAI Codex: Save as
AGENTS.mdin your project atAGENTS.md. - Windsurf: Save as
AGENTS.mdin your project atAGENTS.md. - Cline: Save as
AGENTS.mdin your project atAGENTS.md.
Configuration
AGENTS.md
1# Secure Development Principles23Security-first defaults for any codebase. This config covers input validation, authentication, secrets management, injection prevention, and dependency hygiene. It supplements a stack-specific config — it does not replace one.45## Quick Reference67| Threat | Mitigation | Section |8|---|---|---|9| SQL injection | Parameterized queries, ORMs | [Injection Prevention](#injection-prevention) |10| XSS | Output encoding, CSP, no `dangerouslySetInnerHTML`/`innerHTML` | [XSS Prevention](#xss-prevention) |11| CSRF | SameSite cookies, anti-CSRF tokens | [CSRF Protection](#csrf-protection) |12| SSRF | Allowlists, URL validation, no raw user URLs | [SSRF Prevention](#ssrf-prevention) |13| Leaked secrets | Env vars, no hardcoded credentials, `.gitignore` | [Secrets Management](#secrets-management) |14| Dependency vulns | Automated scanning, lockfiles, pinned versions | [Dependency Hygiene](#dependency-hygiene) |15| Broken auth | Session validation, token expiry, rate limiting | [Authentication & Authorization](#authentication--authorization) |16| PII in logs | Structured logging, redaction | [Logging Safety](#logging-safety) |1718---1920## Input Validation2122Never trust input. Validate at the boundary — every HTTP handler, message consumer, and CLI parser.2324### Principles2526- **Validate type, shape, and range.** A field being present is not enough. Check length, format, and allowed values.27- **Reject first, accept second.** Default-deny: if input doesn't match the expected schema, reject it.28- **Validate on the server.** Client-side validation is UX. Server-side validation is security. Always do both.29- **Use a schema library.** Hand-written validation has gaps.3031### Python (Pydantic)3233```python34from pydantic import BaseModel, Field, field_validator35import re3637class CreateUserRequest(BaseModel):38 email: str = Field(..., max_length=254)39 username: str = Field(..., min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_-]+$")40 age: int = Field(..., ge=13, le=150)4142 @field_validator("email")43 @classmethod44 def validate_email_format(cls, v: str) -> str:45 if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", v):46 raise ValueError("Invalid email format")47 return v.lower().strip()48```4950### TypeScript (Zod)5152```typescript53import { z } from "zod";5455const createUserSchema = z.object({56 email: z.string().email().max(254).transform((v) => v.toLowerCase().trim()),57 username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_-]+$/),58 age: z.number().int().min(13).max(150),59});6061type CreateUserInput = z.infer<typeof createUserSchema>;6263// In the handler:64const parsed = createUserSchema.safeParse(req.body);65if (!parsed.success) {66 return res.status(400).json({ error: parsed.error.flatten() });67}68const { email, username, age } = parsed.data; // Typed and validated69```7071### Common Validation Gaps7273- **File uploads:** validate MIME type (from content, not extension), file size, and filename (strip path traversal characters like `../`).74- **Pagination:** cap `limit` to a max (e.g., 100). Validate `offset` is non-negative. Unbounded pagination is a DoS vector.75- **IDs:** validate format (UUID, integer) before passing to a query. Don't assume the database will reject malformed IDs gracefully.76- **Redirect URLs:** validate against an allowlist of domains. Open redirects enable phishing.7778---7980## Injection Prevention8182### SQL Injection8384**Always use parameterized queries.** This is the single most important security rule for any app that talks to a database.8586```python87# ✅ Parameterized (Python — psycopg2 / asyncpg)88cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))8990# ✅ Parameterized (Python — SQLAlchemy)91stmt = select(User).where(User.id == user_id)9293# ❌ String concatenation — SQL INJECTION94cursor.execute(f"SELECT * FROM users WHERE id = '{user_id}'")9596# ❌ f-string in query — SQL INJECTION97cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")98```99100```typescript101// ✅ Parameterized (TypeScript — pg / node-postgres)102const result = await pool.query("SELECT * FROM users WHERE id = $1", [userId]);103104// ✅ Parameterized (TypeScript — Prisma)105const user = await prisma.user.findUnique({ where: { id: userId } });106107// ❌ Template literal — SQL INJECTION108const result = await pool.query(`SELECT * FROM users WHERE id = '${userId}'`);109```110111**Rules:**112- Never interpolate user input into SQL strings. Not with f-strings, not with template literals, not with string concatenation.113- ORMs (SQLAlchemy, Prisma, Django ORM) handle parameterization. Use them. If you need raw SQL, use the ORM's raw query interface with parameter binding.114- `LIKE` clauses need escaping too: `%` and `_` are wildcards. Escape them in user input before passing to `LIKE`.115- Dynamic column names and `ORDER BY` cannot be parameterized. Validate against an allowlist:116117```python118ALLOWED_SORT_COLUMNS = {"name", "created_at", "email"}119120def get_users(sort_by: str):121 if sort_by not in ALLOWED_SORT_COLUMNS:122 raise ValueError(f"Invalid sort column: {sort_by}")123 return db.execute(text(f"SELECT * FROM users ORDER BY {sort_by}")) # Safe — validated124```125126### Command Injection127128- Never pass user input to `os.system()`, `subprocess.run(shell=True)`, or `child_process.exec()`.129- Use array-form subprocess calls: `subprocess.run(["convert", filename])` (Python) or `execFile("convert", [filename])` (Node.js).130- Validate filenames against an allowlist pattern. Strip `..`, `/`, `\`, and null bytes.131132### Path Traversal133134```python135# ✅ Safe — resolve and check prefix136import pathlib137138UPLOAD_DIR = pathlib.Path("/app/uploads").resolve()139140def get_file(filename: str) -> pathlib.Path:141 filepath = (UPLOAD_DIR / filename).resolve()142 if not filepath.is_relative_to(UPLOAD_DIR):143 raise ValueError("Path traversal detected")144 return filepath145146# ❌ Vulnerable — no path validation147def get_file(filename: str) -> str:148 return f"/app/uploads/{filename}" # filename="../../etc/passwd" escapes149```150151---152153## XSS Prevention154155### Output Encoding156157- Use your framework's built-in templating (React JSX, Jinja2 autoescaping, Django templates). They escape by default.158- Never use `dangerouslySetInnerHTML` (React) or `innerHTML` (vanilla JS) with user-provided content.159- If you must render user HTML (e.g., markdown preview), sanitize with a library like DOMPurify (JS) or bleach (Python).160161```typescript162// ✅ Safe — React auto-escapes163function Comment({ text }: { text: string }) {164 return <p>{text}</p>; // Safe even if text contains <script>165}166167// ❌ XSS — raw HTML injection168function Comment({ html }: { html: string }) {169 return <p dangerouslySetInnerHTML={{ __html: html }} />; // Only with sanitized input170}171172// ✅ Safe — sanitize before rendering173import DOMPurify from "dompurify";174175function Comment({ html }: { html: string }) {176 return <p dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;177}178```179180### Content Security Policy (CSP)181182Set CSP headers to restrict script sources. At minimum:183184```185Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self';186```187188- No `'unsafe-eval'` in `script-src`. If a dependency requires it, find an alternative.189- Use nonce-based CSP for inline scripts when needed.190- Test CSP in report-only mode first: `Content-Security-Policy-Report-Only`.191192---193194## CSRF Protection195196- Set `SameSite=Lax` (or `Strict`) on all authentication cookies. This is the primary defense.197- For state-changing API endpoints that use cookie auth: require an anti-CSRF token (sync token pattern or double-submit cookie).198- APIs using `Authorization: Bearer` headers (not cookies) are not vulnerable to CSRF — browsers don't attach custom headers in cross-origin form submissions.199- Verify the `Origin` header on state-changing requests matches your domain.200201---202203## SSRF Prevention204205Server-Side Request Forgery happens when your server fetches a URL provided by the user.206207```python208# ❌ Vulnerable — user controls the URL209response = requests.get(user_provided_url)210211# ✅ Safe — allowlist of domains212from urllib.parse import urlparse213214ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}215216def safe_fetch(url: str) -> requests.Response:217 parsed = urlparse(url)218 if parsed.hostname not in ALLOWED_HOSTS:219 raise ValueError(f"Host not allowed: {parsed.hostname}")220 if parsed.scheme not in ("http", "https"):221 raise ValueError(f"Scheme not allowed: {parsed.scheme}")222 return requests.get(url, timeout=10)223```224225**Rules:**226- Never fetch arbitrary user-provided URLs. Allowlist permitted hosts.227- Block access to internal IPs (`127.0.0.1`, `169.254.169.254`, `10.x`, `172.16-31.x`, `192.168.x`).228- Set timeouts on all outbound requests. Without them, an SSRF can tie up server resources indefinitely.229- If you accept webhook URLs from users, validate at registration time and resolve DNS to ensure it's not internal.230231---232233## Authentication & Authorization234235### Authentication Rules236237- **Hash passwords with bcrypt, scrypt, or Argon2.** Never MD5 or SHA-256 alone. Use the library defaults for cost factors.238- **Session tokens:** cryptographically random, at least 128 bits of entropy. Use your framework's session library.239- **JWTs:** validate signature, expiry (`exp`), issuer (`iss`), and audience (`aud`) on every request. Use a library — never parse JWTs manually.240- **Token storage:** `HttpOnly`, `Secure`, `SameSite=Lax` cookies for web apps. `localStorage` for JWTs only if no cookie option exists (XSS risk).241- **Rate limit login endpoints.** 5 attempts per minute per IP/account is a reasonable starting point.242243### Authorization Rules244245- **Check permissions on every request.** Middleware or decorators, not ad-hoc checks in handler bodies.246- **Check object-level access.** "User is authenticated" is not enough. Verify the user owns (or has access to) the specific resource.247- **Default deny.** If there's no explicit policy allowing access, deny.248249```python250# ❌ Insecure — checks auth but not ownership251@app.get("/orders/{order_id}")252async def get_order(order_id: str, current_user: User):253 return await db.get(Order, order_id) # Any user can view any order254255# ✅ Secure — checks ownership256@app.get("/orders/{order_id}")257async def get_order(order_id: str, current_user: User):258 order = await db.get(Order, order_id)259 if not order or order.user_id != current_user.id:260 raise NotFoundError("Order", order_id) # 404, not 403 — don't leak existence261 return order262```263264- Return `404` (not `403`) when a user lacks access to a resource. `403` confirms the resource exists.265266---267268## Secrets Management269270### Rules271272- **Never hardcode secrets.** No API keys, database passwords, or tokens in source code.273- **Use environment variables** for local development. Use a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) in production.274- **`.env` files are never committed.** Add `.env` to `.gitignore`. Provide `.env.example` with placeholder values.275- **Rotate secrets regularly.** Design systems to support rotation without downtime (e.g., accept both old and new keys during transition).276- **Least privilege.** Database credentials should have only the permissions the app needs. Don't use the superuser account.277278### Pre-Commit Check279280Add a pre-commit hook or CI step that scans for accidental secret commits:281282```yaml283# .pre-commit-config.yaml284repos:285 - repo: https://github.com/gitleaks/gitleaks286 rev: v8.18.0287 hooks:288 - id: gitleaks289```290291Patterns to catch: API keys, AWS credentials, private keys, database URLs with passwords.292293### If a Secret is Committed294295**Rotate immediately.** Assume it's compromised — even in a private repo, even if you force-push. Git history retains it. Remove from code, add to `.gitignore`, audit access logs. `git filter-repo` can rewrite history, but rotation is the real fix.296297---298299## Dependency Hygiene300301### Rules302303- **Pin exact versions** in lockfiles (`package-lock.json`, `poetry.lock`, `go.sum`). Commit lockfiles.304- **Update regularly.** Stale dependencies accumulate CVEs. Run updates monthly at minimum.305- **Audit before adding.** Before adding a dependency: check download counts, maintenance activity, and known vulnerabilities. A small dependency with one maintainer is a supply chain risk.306- **Minimize dependencies.** Every dependency is attack surface. If the stdlib or a one-liner replaces it, don't add a package.307308### Automated Scanning309310Enable at least one of:311312| Tool | Ecosystem | Setup |313|---|---|---|314| `npm audit` / `pnpm audit` | Node.js | Built-in, run in CI |315| `pip-audit` | Python | `pip install pip-audit && pip-audit` |316| `govulncheck` | Go | `go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...` |317| Dependabot / Renovate | All | Enable in GitHub/GitLab settings |318| Snyk / Socket | All | Integrates with CI |319320Run dependency audits in CI. Block merges on critical/high severity vulnerabilities.321322---323324## Logging Safety325326### What to Log327328- Request method, path, status code, response time.329- Authentication events (login success, login failure, token refresh).330- Authorization failures (user tried to access a resource they don't own).331- Business-critical state changes (order placed, payment processed, account deleted).332- Error details: type, message, stack trace (server-side only).333334### What to NEVER Log335336- Passwords (even hashed ones).337- Session tokens, API keys, JWTs.338- Credit card numbers, SSNs, or other PII.339- Full request/response bodies (may contain any of the above).340- Database connection strings.341342### Structured Logging343344Use structured logging (JSON) so log aggregation tools can parse and query fields:345346```python347# Python (structlog)348import structlog349logger = structlog.get_logger()350351logger.info("order_created", order_id=order.id, user_id=user.id, total_cents=order.total)352353# ❌ Never:354logger.info(f"Order created: {order}") # May serialize sensitive fields355logger.info(f"User {user.email} logged in with token {token}") # PII + secret356```357358```typescript359// TypeScript (pino)360import pino from "pino";361const logger = pino();362363logger.info({ orderId: order.id, userId: user.id }, "order_created");364365// ❌ Never:366logger.info(`User ${user.email} logged in with token ${token}`);367```368369### Error Responses370371- Return generic error messages to clients: `"Something went wrong"`, not `"NullPointerException at UserService.java:142"`.372- Log the full error server-side. Return only a correlation ID to the client so support can look it up.373- Never expose stack traces, SQL queries, or internal paths in API responses.374375---376377## Security Headers378379Set these on all responses (via middleware or reverse proxy):380381```382Strict-Transport-Security: max-age=63072000; includeSubDomains; preload383X-Content-Type-Options: nosniff384X-Frame-Options: DENY385Referrer-Policy: strict-origin-when-cross-origin386Permissions-Policy: camera=(), microphone=(), geolocation=()387```388389- `HSTS`: forces HTTPS. Only enable after confirming HTTPS works everywhere.390- `X-Content-Type-Options`: prevents MIME sniffing. Always set.391- `X-Frame-Options: DENY`: prevents clickjacking. Use `SAMEORIGIN` if you need iframes from your own domain.392393---394395## Pull Request Security Checklist396397Before merging any change:3983991. No hardcoded secrets, API keys, or credentials in the diff.4002. All user input is validated (type, length, format) at the server boundary.4013. Database queries use parameterized statements. No string interpolation in SQL.4024. New endpoints have authentication and authorization checks.4035. Error responses don't leak internal details (stack traces, SQL, file paths).4046. New dependencies have been reviewed for maintenance status and known vulnerabilities.4057. Logging doesn't include PII, tokens, or secrets.4068. File upload endpoints validate type, size, and sanitize filenames.4079. Any new redirect accepts only allowlisted destinations.40810. `pnpm audit` / `pip-audit` / `govulncheck` shows no new critical/high issues.409
Community feedback
0 found this helpful
Works with: