Home
cd ../playbooks
Developer ToolsIntermediate

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.

10 minutes
By Addy OsmaniSource
#security#secure-coding#owasp#threat-modeling#vulnerability-prevention#code-quality

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

CLAUDE.md Template

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.

README.md

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.

$Related Playbooks

Developer Tools

Writing for AI Agents: A Craft Reference

A theory of writing any document an agent consumes — skills, CLAUDE.md, reference docs — built on context pointers, an information hierarchy of steps versus disclosed reference, completion criteria, and leading words that anchor behavior in the fewest tokens.

10 minutes
Advanced
Developer Tools

Security Guidance Review

Three-layer continuous security review for AI-generated code — instant regex warnings on edit, an LLM diff review at end of turn, and an agentic commit-time reviewer that traces data flow across files.

10 minutes
Advanced
Developer Tools

Shannon: Autonomous Pentesting for Your Own Apps

An operating guide for driving Keygraph's Shannon CLI: scope a white-box pentest against an app you own, run it, and turn the proven findings into fix tasks

15 minutes
Advanced
Developer Tools

Task Observer: One Skill to Rule Them All

A meta-skill that watches every work session, logs corrections and workflow patterns as skill candidates, and runs a review cycle that turns the log into new or improved skills

15 minutes
Advanced
Developer Tools

Soft UI Design Skill: Premium, Awwwards-Tier Interfaces

A design system that makes Claude build $150k-agency-feeling UI — double-bezel cards, spring-physics motion, magnetic buttons, and a banned list that blocks every cheap AI-design tell

5 minutes
Intermediate
Developer Tools

Stitch Design Taste: Semantic DESIGN.md Generator for Google Stitch

Generates a DESIGN.md that encodes premium, anti-generic design rules in Google Stitch's natural-language format — color, type, layout, motion intent, and a full banned-pattern list

5 minutes
Intermediate
Developer Tools

Taste Skill v1: Legacy Anti-Slop Frontend Design

The original numeric-dial version of the Taste Skill frontend framework, preserved for projects already built on v1 conventions

10 minutes
Intermediate
Developer Tools

Taste Skill: Anti-Slop Frontend Design

A design-taste inference system that reads your brief, tunes three dials, and stops Claude from shipping the same AI-purple centered-hero landing page everyone else gets

10 minutes
Intermediate
Developer Tools

Scrapling Web Extractor

Install, troubleshoot, and use the Scrapling CLI to extract HTML, Markdown, or text from webpages — including browser-backed fetching and tricky sources like WeChat articles.

10 minutes
Beginner
Developer Tools

Statusline Generator

Install, configure, customize, or troubleshoot the Claude Code statusline — cwd, model, token counts, cost via ccusage, git status — and fix a statusline that's blank, stuck, or silent.

10 minutes
Beginner
Developer Tools

Terminal Screenshot

Render a terminal program's colored output to a PNG so Claude can actually see the visual result — color contrast, alignment, highlighting — instead of only reading raw ANSI escape codes.

5 minutes
Beginner
Developer Tools

Terraform Operational Traps

Avoid the operational traps in Terraform provisioners, multi-environment isolation, and zero-to-deployment reliability — provisioner failures, snapshot cross-contamination, and post-apply container issues.

20 minutes
Advanced

Browse all Developer Tools playbooks →