Security testing is most effective when it’s embedded in the development workflow — not left to a quarterly pentest. Claude Code integrates security checks into daily development: reviewing code for OWASP vulnerabilities as it’s written, scanning dependencies for CVEs, detecting secrets before they’re committed, and writing security-focused test cases that verify attack resistance.
This guide covers security testing with Claude Code: OWASP Top 10 patterns, automated scanning integration, secrets detection, and security-focused testing.
Security Code Review
Review this API endpoint for security vulnerabilities.
Focus on injection attacks, authentication bypass, and data exposure.
Claude Code applies a systematic security lens to code. Common findings on API handlers:
1. Missing input validation (Injection)
// Vulnerable — direct user input in query
app.get('/users', async (req, res) => {
const { role } = req.query;
const users = await db.query(`SELECT * FROM users WHERE role = '${role}'`); // SQL injection
res.json(users);
});
// Secure — parameterized query + enum validation
app.get('/users', async (req, res) => {
const role = z.enum(['admin', 'user', 'moderator']).parse(req.query.role);
const users = await db.query('SELECT id, name, email FROM users WHERE role = $1', [role]);
res.json(users);
});
2. Excessive data exposure
// Vulnerable — returns all user fields including sensitive ones
const user = await User.findById(id);
res.json(user); // Includes: password_hash, 2fa_secret, stripe_customer_id
// Secure — explicit field selection
const user = await User.findById(id).select('id name email createdAt');
res.json(user);
3. Missing authorization (IDOR)
// Vulnerable — any authenticated user can access any order
app.get('/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
// Secure — verify ownership
app.get('/orders/:id', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id, // Must belong to requesting user
});
if (!order) return res.status(404).json({ error: 'Order not found' });
res.json(order);
});
CLAUDE.md Security Rules
## Security Rules (enforced in code review)
- All SQL queries: parameterized only — no string interpolation
- All user input: validated with zod schema before use
- All API responses: explicit field selection — never return raw DB objects
- All authenticated endpoints: verify resource ownership (userId check)
- CORS: explicit allowlist — no wildcard in production
- Rate limiting: all auth endpoints, all user-facing APIs
- Error messages: never expose stack traces or DB details to clients
- File uploads: validate MIME type + extension, store outside webroot, scan for malware
Automated SAST Integration
Add SAST scanning to our CI pipeline.
Fail the build on high-severity findings.
# .github/workflows/security.yml
name: Security Scanning
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
sast:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
# Semgrep — rule-based SAST (fast, low false positives)
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/typescript
p/nodejs
p/secrets
generateSarif: true
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
# Upload results to GitHub Security tab
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Audit npm dependencies
run: npm audit --audit-level=high
# Snyk for more comprehensive scanning
- name: Snyk dependency scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
Secrets Detection
We accidentally committed an API key 3 months ago.
Set up prevention so it never happens again.
Pre-commit hook with detect-secrets:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# Initialize the secrets baseline (marks known false positives as OK)
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Install the git hooks
pip install pre-commit
pre-commit install
GitHub Actions to scan existing history:
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for historical scan
- name: Scan for secrets with gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
Custom patterns for your specific secrets:
# .gitleaks.toml
[extend]
useDefault = true
[[rules]]
description = "Our internal API key format"
regex = '''MYAPP_[A-Z0-9]{32}'''
tags = ["api-key", "internal"]
[[rules]]
description = "Stripe secret key"
regex = '''sk_(test|live)_[0-9a-zA-Z]{24,}'''
tags = ["stripe", "payment"]
Security-Focused Testing
Write tests that specifically verify our auth system
can't be bypassed. Test for common attack patterns.
// tests/security/auth.test.ts
describe('Authentication security', () => {
describe('JWT tampering', () => {
it('rejects tokens with modified payload', async () => {
const validToken = await generateToken({ userId: '123', role: 'user' });
// Decode and modify payload without valid signature
const parts = validToken.split('.');
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
payload.role = 'admin'; // Privilege escalation attempt
const tamperedToken = `${parts[0]}.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.${parts[2]}`;
const response = await request(app)
.get('/api/admin/users')
.set('Authorization', `Bearer ${tamperedToken}`);
expect(response.status).toBe(401);
});
it('rejects expired tokens', async () => {
const expiredToken = jwt.sign(
{ userId: '123', role: 'user' },
process.env.JWT_SECRET!,
{ expiresIn: '-1s' }, // Already expired
);
const response = await request(app)
.get('/api/me')
.set('Authorization', `Bearer ${expiredToken}`);
expect(response.status).toBe(401);
});
});
describe('IDOR prevention', () => {
it('prevents accessing another user\'s orders', async () => {
const user1Token = await loginAs('[email protected]');
const user2Order = await createOrderForUser('[email protected]');
const response = await request(app)
.get(`/api/orders/${user2Order.id}`)
.set('Authorization', `Bearer ${user1Token}`);
expect(response.status).toBe(404); // Not 403 — don't reveal existence
});
});
describe('SQL injection prevention', () => {
it('handles malicious input in search', async () => {
const maliciousInput = "'; DROP TABLE users; --";
const response = await request(app)
.get('/api/search')
.query({ q: maliciousInput })
.set('Authorization', `Bearer ${validToken}`);
// Should return empty results, not error
expect(response.status).toBe(200);
expect(response.body.results).toEqual([]);
// Verify users table is still intact
const userCount = await db('users').count('*');
expect(Number(userCount[0].count)).toBeGreaterThan(0);
});
});
describe('Rate limiting', () => {
it('blocks brute force login attempts', async () => {
const attempts = Array(10).fill(null).map(() =>
request(app).post('/api/auth/login').send({
email: '[email protected]',
password: 'wrong-password',
})
);
const responses = await Promise.all(attempts);
// After 5 failures, should be rate limited
const rateLimitedCount = responses.filter(r => r.status === 429).length;
expect(rateLimitedCount).toBeGreaterThan(0);
});
it('returns Retry-After header when rate limited', async () => {
// Exceed rate limit
for (let i = 0; i < 6; i++) {
await request(app).post('/api/auth/login').send({ email: '[email protected]', password: 'wrong' });
}
const response = await request(app)
.post('/api/auth/login')
.send({ email: '[email protected]', password: 'wrong' });
expect(response.status).toBe(429);
expect(response.headers['retry-after']).toBeDefined();
});
});
});
XSS Prevention Testing
Write tests that verify our input sanitization
prevents stored XSS attacks.
describe('XSS prevention', () => {
const xssPayloads = [
'<script>alert("xss")</script>',
'<img src="x" onerror="alert(1)">',
'javascript:alert(1)',
'<svg onload="alert(1)">',
'"><script>alert(1)</script>',
];
it.each(xssPayloads)('sanitizes XSS payload in comment: %s', async (payload) => {
const token = await loginTestUser();
// Submit payload as user content
const createResponse = await request(app)
.post('/api/comments')
.set('Authorization', `Bearer ${token}`)
.send({ content: payload });
expect(createResponse.status).toBe(201);
const commentId = createResponse.body.id;
// Retrieve and verify it's sanitized
const getResponse = await request(app)
.get(`/api/comments/${commentId}`)
.set('Authorization', `Bearer ${token}`);
const savedContent = getResponse.body.content;
// Should not contain executable script tags
expect(savedContent).not.toMatch(/<script/i);
expect(savedContent).not.toMatch(/javascript:/i);
expect(savedContent).not.toMatch(/onerror=/i);
expect(savedContent).not.toMatch(/onload=/i);
});
});
Dependency Vulnerability Management
We have 15 high-severity CVEs in our dependencies.
Help me triage and remediate them efficiently.
# Audit and output structured report
npm audit --json > audit-results.json
Parse this npm audit JSON output.
Group findings by: fix available (update), fix requires major version bump (review),
and no fix (evaluate alternative package).
Prioritize by severity and whether the vulnerable code path is actually reachable.
Claude Code parses the audit output and generates a prioritized remediation plan:
## Remediation Plan
### Auto-fix (run npm audit fix):
- lodash: 4.17.15 → 4.17.21 (PROTOTYPE_POLLUTION, patch available)
- follow-redirects: 1.14.7 → 1.15.4 (URL_REDIRECT, patch available)
### Manual update required (semver major — review for breaking changes):
- axios: 0.21.1 → 1.6.0 (SSRF, major version bump — check for API changes)
Breaking changes in v1.x: [list of changes]
### No fix available — evaluate alternatives:
- node-forge: CVE-2022-24771 (RSA padding oracle) — replace with native crypto module
Estimated effort: 4 hours
Affected code: src/lib/certificate.ts
For the full security audit methodology including OWASP Top 10 and CSP headers, see the security audit guide. For code review with security criteria embedded in the process, see the code review guide. The Claude Skills 360 bundle includes a security testing skill set for automated OWASP scanning, penetration testing patterns, and compliance checklists. Start with the free tier to run a security scan on your codebase.