Secure Coding Practices
A threat-model-first secure coding reference — trust-boundary mapping, a STRIDE quick-pass, a three-tier always/ask-first/never boundary system, and copy-paste prevention patterns for injection, XSS, broken access control, and SSRF.
An AI assistant can write a feature that compiles, passes the demo, and quietly uses string-concatenated SQL or fetches whatever URL the user handed it — because nobody told it to think about trust boundaries before writing the first line.
Who it's for: developers using AI coding assistants on production codebases, teams without a dedicated security engineer who still need secure defaults, backend engineers building anything that touches user input, authentication, or external APIs, anyone who wants a concrete always-do/ask-first/never-do checklist instead of vague security advice
Example
"Add a webhook receiver that fetches the callback URL" → A five-minute trust-boundary map before any code, a STRIDE pass flagging SSRF as the live threat, an allowlisted-and-DNS-checked fetch pattern instead of a naive fetch(userUrl), and a reminder to run the project's dependency audit before shipping
New here? 3-minute setup guide → | Already set up? Copy the template below.
# Secure Coding Practices
Security-first development practices. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
## When to Use
- Building anything that accepts user input
- Implementing authentication or authorization
- Storing or transmitting sensitive data
- Integrating with external APIs or services
- Adding file uploads, webhooks, or callbacks
- Handling payment or PII data
## Process: Threat Model First
Controls bolted on without a threat model are guesses. Before hardening anything, spend five minutes thinking like an attacker:
1. **Map the trust boundaries.** Where does untrusted data cross into the system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and **LLM output** — plus local values that look internal because the OS handed them over: another process's command line or environment, filenames on a shared volume, a path in a job payload. Trust follows who *wrote* a value, not which channel delivered it. Every boundary is attack surface.
2. **Name the assets.** What's worth stealing or breaking? Credentials, PII, payment data, admin actions, money movement.
3. **Run STRIDE over each boundary** — a quick lens, not a ceremony:
| Threat | Ask | Typical mitigation |
|---|---|---|
| **S**poofing | Can someone impersonate a user/service? | Authentication, signature verification |
| **T**ampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS |
| **R**epudiation | Can an action be denied later? | Audit logging of security events |
| **I**nformation disclosure | Can data leak? | Encryption, field allowlists, generic errors |
| **D**enial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts |
| **E**levation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |
4. **Write abuse cases next to use cases.** For each feature, ask "how would I misuse this?" — then make that your first test.
If you can't name the trust boundaries for a feature, you're not ready to secure it. Most breaches begin in design, not code.
## The Three-Tier Boundary System
### Always Do (No Exceptions)
- **Validate all external input** at the system boundary (API routes, form handlers)
- **Parameterize all database queries** — never concatenate user input into SQL
- **Encode output** to prevent XSS (use framework auto-escaping, don't bypass it)
- **Use HTTPS** for all external communication
- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext)
- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- **Use httpOnly, secure, sameSite cookies** for sessions
- **Run the project's package-manager audit** against the committed lockfile before every release
### Ask First (Requires Human Approval)
- Adding new authentication flows or changing auth logic
- Storing new categories of sensitive data (PII, payment info)
- Adding new external service integrations
- Changing CORS configuration
- Adding file upload handlers
- Modifying rate limiting or throttling
- Granting elevated permissions or roles
### Never Do
- **Never commit secrets** to version control (API keys, passwords, tokens)
- **Never log sensitive data** (passwords, tokens, full credit card numbers)
- **Never trust client-side validation** as a security boundary
- **Never disable security headers** for convenience
- **Never use `eval()` or `innerHTML`** with user-provided data
- **Never store sessions in client-accessible storage** (localStorage for auth tokens)
- **Never expose stack traces** or internal error details to users
## Prevention Patterns by Vulnerability Class
### Injection (SQL, NoSQL, OS Command)
```typescript
// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });
```
### Broken Authentication
```typescript
import { hash, compare } from 'bcrypt';
const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);
app.use(session({
secret: process.env.SESSION_SECRET, // From environment, not code
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000,
},
}));
```
### Cross-Site Scripting (XSS)
```typescript
// BAD
element.innerHTML = userInput;
// GOOD: framework auto-escaping (React does this by default)
return <div>{userInput}</div>;
// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
```
### Broken Access Control
```typescript
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
const task = await taskService.findById(req.params.id);
// Check authorization, not just authentication
if (task.ownerId !== req.user.id) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
});
}
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});
```
### Security Misconfiguration
```typescript
import helmet from 'helmet';
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
},
}));
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));
```
### Sensitive Data Exposure
```typescript
function sanitizeUser(user: UserRecord): PublicUser {
const { passwordHash, resetToken, ...publicFields } = user;
return publicFields;
}
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
```
### Server-Side Request Forgery (SSRF)
Any time the server fetches a URL a user influenced — webhooks, "import from URL", image proxies, link previews — an attacker can aim it at internal services (cloud metadata, `localhost`, private IPs).
```typescript
// BAD: fetch whatever the user gives you
await fetch(req.body.webhookUrl);
// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';
async function safeFetch(rawUrl: string) {
const url = new URL(rawUrl);
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Blocked scheme');
const { address } = await lookup(url.hostname);
const addr = ipaddr.parse(address);
if (addr.range() !== 'unicast') throw new Error('Blocked internal address');
return fetch(url.toString(), { redirect: 'error' });
}
```
## Dependency and Supply-Chain Hygiene
- Run the ecosystem's native audit command (`npm audit`, `pip-audit`, `cargo audit`, `bundle audit`) against the committed lockfile before every release, not on an ad-hoc basis.
- Pin dependency versions in the lockfile; don't let CI resolve a fresh dependency tree on every run.
- Treat a new transitive dependency the same as a new direct one when it shows up in an audit — check its maintenance status and download count before accepting the fix.
- For LLM-assisted development specifically: an AI-generated dependency suggestion should be verified to actually exist and be the intended package before it's added — package-name hallucination and typosquatting are real risks when suggestions come from a model instead of a human's memory.
## Tips
- Do the five-minute threat model even when it feels like overkill for a small feature — the boundaries you skip mapping are exactly the ones that turn into incidents.
- "Handle errors" is not a security control — name the specific failure mode (a missing auth header, a malformed webhook payload) and what happens for it.
- Generic error messages to the user, detailed errors to the server log — never the reverse.
## Limitations
- This is a practices reference, not an automated scanner — it guides what to build and review for, but doesn't replace SAST/DAST tooling or a professional penetration test for anything handling real money or regulated data.
- The three-tier boundary system assumes a team that can actually enforce "ask first" — in a solo or fast-moving context, treat that tier as a personal checklist rather than a gate.
- Framework-specific examples use a Node/TypeScript stack; translate the underlying pattern (parameterize, encode, authorize, allowlist) to whatever stack the project actually uses.
Get new playbooks like this one
One email a week with new Claude Code workflows. Free, like everything here.
No spam. Unsubscribe anytime.
What This Does
A secure-by-default coding reference built around a five-minute threat model — map the trust boundaries, name the assets, run a quick STRIDE pass, write an abuse case next to every use case — done before writing the feature, not bolted on after. From there it gives a concrete three-tier boundary system: what to always do without exception (parameterized queries, hashed passwords, security headers), what requires asking first (new auth flows, new data categories, new integrations), and what to never do (commit secrets, trust client-side validation, expose stack traces).
The bulk of the reference is copy-paste prevention patterns for the vulnerability classes that show up most in day-to-day code — SQL injection, broken authentication, XSS, broken access control, security misconfiguration, sensitive data exposure, and SSRF (the one attackers reach for whenever a server fetches a user-influenced URL) — plus a dependency and supply-chain hygiene section covering lockfile audits and the specific risk of an AI-suggested package that doesn't actually exist.
Quick Start
Step 1: Create a Project Folder
mkdir secure-coding && cd secure-coding
Step 2: Download the Template
Click Download above, then:
mv ~/Downloads/CLAUDE.md ./
Step 3: Apply It Before Building a Feature
claude
Describe the feature you're about to build — especially anything touching user input, auth, or an external service — and ask Claude to threat-model it first using this reference before writing code.
Tips & Best Practices
- Do the five-minute threat model even when a feature feels too small for it — the boundary you skip mapping is exactly the one that becomes the incident.
- "Handle errors" is not a security control on its own — name the specific failure mode (a missing auth header, a malformed webhook payload) and what happens for each.
- Show generic errors to the user and detailed ones to the server log, never the reverse — stack traces are a gift to an attacker probing for weaknesses.
- Treat an AI-suggested dependency the same as any other new dependency: verify it actually exists and is the intended package before adding it — hallucinated or typosquatted package names are a real supply-chain risk.
Limitations
- A practices reference, not an automated scanner — it shapes what to build and review for, but doesn't replace SAST/DAST tooling or a professional penetration test for anything handling real money or regulated data.
- The "ask first" tier assumes a team that can actually enforce a gate; in a solo or fast-moving context, treat it as a personal checklist rather than an approval workflow.
- Code examples use a Node/TypeScript stack — translate the underlying pattern (parameterize, encode, authorize, allowlist) to whatever stack the project actually uses.