Security Review Prompt Templates

AI prompt templates for security code reviews. Identify vulnerabilities and implement secure coding practices.

Overview

Security vulnerabilities often hide in plain sight, injection flaws, authentication bypasses, and data exposure that seem obvious in retrospect. These prompts help you think like an attacker to find vulnerabilities before they're exploited. They're for defensive purposes: identifying weaknesses in your own code and implementing secure patterns.

Best Practices

1

Specify what the code handles (user input, payments, auth) so security review focuses on relevant threats

2

Include the full request flow, not just the handler, vulnerabilities often exist at boundaries

3

Mention your security requirements (compliance, data sensitivity) for appropriate depth

4

Describe your deployment environment (public internet vs. internal) to assess exposure

5

Ask for specific vulnerability classes (OWASP Top 10) rather than just 'review for security'

Prompt Templates

1. Security Vulnerability Assessment

Review this code for security vulnerabilities:

```[LANGUAGE]
[PASTE CODE]
```

**This code handles:**
- [WHAT IT PROCESSES - e.g. user registration, file uploads, payments]

**Exposure:**
- Internet-facing: [YES/NO]
- Authenticated: [REQUIRED/OPTIONAL/NONE]
- User input: [DESCRIBE WHAT INPUT IS ACCEPTED]

**Check for:**
- Injection vulnerabilities (SQL, command, XSS)
- Authentication/authorization flaws
- Sensitive data exposure
- Security misconfigurations
- Broken access control

**Rate each finding:** Critical / High / Medium / Low
**Provide:** Proof of concept (how to exploit) and secure fix
Language: Node.js/Express
Code: User login endpoint with password check
Handles: User authentication
Exposure: Internet-facing, no prior auth required
User input: Email and password from POST body
## Security Vulnerabilities Found

### 1. SQL Injection (Critical)

**Vulnerable code (Line 12):**
```javascript
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
```

**Exploitation:**
```bash
curl -X POST /login -d "email=' OR '1'='1' --&password=anything"
# Returns first user in database, likely admin
```

**Secure fix:**
```javascript
const query = 'SELECT * FROM users WHERE email = ?';
const [rows] = await db.execute(query, [req.body.email]);
```

---

### 2. Timing Attack on Password Comparison (High)

**Vulnerable code (Line 18):**
```javascript
if (user.password === req.body.password) {
```

**Exploitation:** String comparison exits on first mismatch. Attacker can measure response times to guess password character by character.

**Secure fix:**
```javascript
const crypto = require('crypto');

// Constant-time comparison
const isMatch = crypto.timingSafeEqual(
 Buffer.from(storedHash),
 Buffer.from(computedHash)
);
```

---

### 3. No Password Hashing (Critical)

**Issue:** Passwords appear to be stored/compared as plaintext.

**Secure fix:**
```javascript
const bcrypt = require('bcrypt');

// On registration
const hashedPassword = await bcrypt.hash(password, 12);

// On login
const isValid = await bcrypt.compare(req.body.password, user.hashedPassword);
```

---

### 4. Missing Rate Limiting (Medium)

**Issue:** No protection against brute force attacks.

**Secure fix:**
```javascript
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
 windowMs: 15 * 60 * 1000, // 15 minutes
 max: 5, // 5 attempts
 message: 'Too many login attempts, try again later'
});

app.post('/login', loginLimiter, loginHandler);
```

---

### 5. Verbose Error Messages (Low)

**Issue (Line 20):**
```javascript
return res.status(401).json({ error: 'User not found' });
// vs
return res.status(401).json({ error: 'Incorrect password' });
```

**Exploitation:** Tells attacker which emails are registered.

**Secure fix:** Same message for both cases:
```javascript
return res.status(401).json({ error: 'Invalid email or password' });
```

Before deploying code that handles authentication, user input, sensitive data, or external exposure

  • Review authentication code with extra scrutiny, it's the highest-impact target
  • Assume all user input is malicious until validated and sanitized

2. Input Validation Design

Help me design input validation for this endpoint:

**Endpoint:** [METHOD] [PATH]
**Purpose:** [WHAT IT DOES]

**Expected inputs:**
```
[DESCRIBE OR SHOW INPUT FORMAT]
```

**Business rules:**
- [RULE 1 - e.g. email must be unique]
- [RULE 2 - e.g. age must be 18+]

**Threats to mitigate:**
- [SPECIFIC CONCERNS - e.g. prevent script injection in bio field]

**Framework:** [EXPRESS / FASTAPI / LARAVEL / etc.]
**Validation library preference:** [ZOD / JOI / PYDANTIC / etc.]

Provide:
1. Validation schema
2. Sanitization rules
3. Error response format
4. Edge cases to handle
Endpoint: POST /api/users (user registration)
Inputs: { email, password, name, bio, birthdate, website }
Business rules: Email unique, password 8+ chars with complexity, age 13+
Threats: XSS in bio/name, SSRF via website URL
Framework: Express.js
Library: Zod
## Input Validation Schema

```typescript
import { z } from 'zod';
import xss from 'xss';

// Custom refinements
const sanitizeString = (val: string) => xss(val.trim());

const isNotDisposableEmail = (email: string) => {
 const disposableDomains = ['tempmail.com', 'throwaway.com'];
 const domain = email.split('@')[1];
 return !disposableDomains.includes(domain);
};

const isSafeUrl = (url: string) => {
 try {
 const parsed = new URL(url);
 // Prevent SSRF: only allow http(s), no internal IPs
 if (!['http:', 'https:'].includes(parsed.protocol)) return false;
 const ip = parsed.hostname;
 if (ip.startsWith('127.') || ip.startsWith('10.') || 
 ip.startsWith('192.168.') || ip === 'localhost') return false;
 return true;
 } catch {
 return false;
 }
};

// Registration schema
export const CreateUserSchema = z.object({
 email: z
.string()
.email('Invalid email format')
.max(254, 'Email too long')
.toLowerCase()
.refine(isNotDisposableEmail, 'Disposable emails not allowed'),

 password: z
.string()
.min(8, 'Password must be at least 8 characters')
.max(128, 'Password too long') // Prevent DoS via bcrypt
.regex(
 /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
 'Password must contain uppercase, lowercase, and number'
 ),

 name: z
.string()
.min(1, 'Name is required')
.max(100, 'Name too long')
.transform(sanitizeString), // XSS protection

 bio: z
.string()
.max(500, 'Bio too long')
.optional()
.transform((val) => val ? sanitizeString(val) : val),

 birthdate: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format')
.refine((date) => {
 const age = Math.floor(
 (Date.now() - new Date(date).getTime()) / (365.25 * 24 * 60 * 60 * 1000)
 );
 return age >= 13;
 }, 'Must be at least 13 years old'),

 website: z
.string()
.url('Invalid URL')
.refine(isSafeUrl, 'Invalid website URL')
.optional(),
});

export type CreateUserInput = z.infer<typeof CreateUserSchema>;
```

## Middleware Usage

```typescript
import { Request, Response, NextFunction } from 'express';

export const validateCreateUser = (
 req: Request,
 res: Response,
 next: NextFunction
) => {
 const result = CreateUserSchema.safeParse(req.body);

 if (!result.success) {
 return res.status(400).json({
 error: 'VALIDATION_ERROR',
 details: result.error.errors.map((e) => ({
 field: e.path.join('.'),
 message: e.message,
 })),
 });
 }

 // Replace body with validated & sanitized data
 req.body = result.data;
 next();
};
```

## Edge Cases Handled

| Input | Handling |
|-------|----------|
| `<script>alert(1)</script>` in name | Sanitized to empty/escaped |
| `http://169.254.169.254/` (AWS metadata) | Rejected by SSRF check |
| Very long password | Capped at 128 to prevent bcrypt DoS |
| Unicode homoglyphs in email | Consider punycode normalization |
| Birthdate `9999-12-31` | Fails age check (negative age) |

When designing input handling for any endpoint that accepts user data

  • Validate on input AND sanitize on output, defense in depth
  • Always have a max length, even for 'unlimited' text fields (prevent DoS)

3. Authentication Flow Review

Review my authentication implementation:

**Auth method:** [SESSION / JWT / OAUTH / etc.]

**Implementation:**
```[LANGUAGE]
[PASTE AUTH CODE - login, token generation, verification]
```

**Token/session storage:**
- Client-side: [COOKIE / LOCALSTORAGE / MEMORY]
- Server-side: [DATABASE / REDIS / STATELESS]

**Protected resources:**
- [WHAT REQUIRES AUTH]

**Check for:**
- Token security (signing, expiration, storage)
- Session management (fixation, hijacking)
- Password handling (hashing, reset flow)
- Account enumeration prevention
- Privilege escalation vectors
Auth method: JWT with refresh tokens
Code: Login endpoint, token generation, middleware verification
Client storage: Access token in memory, refresh token in httpOnly cookie
Server storage: Stateless JWT, refresh tokens in Redis
Protected: All /api/* routes except /api/auth/*
## Authentication Security Review

### ✅ Good Practices Observed

1. **Refresh token in httpOnly cookie** - Prevents XSS from stealing long-lived token
2. **Access token in memory** - Not persisted, cleared on tab close
3. **Short access token expiry (15min)** - Limits window of compromise
4. **Refresh tokens in Redis** - Enables revocation

### ⚠️ Issues Found

#### 1. JWT Secret Weakness (High)

**Issue (Line 8):**
```javascript
const secret = process.env.JWT_SECRET || 'fallback-secret';
```

**Problem:** Fallback secret in code. If env var is missing, all tokens signed with known value.

**Fix:**
```javascript
const secret = process.env.JWT_SECRET;
if (!secret || secret.length < 32) {
 throw new Error('JWT_SECRET must be set and at least 32 characters');
}
```

#### 2. Missing Token Type Validation (High)

**Issue:** Access token and refresh token use same structure.

**Attack:** Use refresh token as access token to bypass expiration.

**Fix:** Add token type claim:
```javascript
// Access token
const accessToken = jwt.sign(
 { userId, type: 'access' },
 secret,
 { expiresIn: '15m' }
);

// Verification middleware
if (decoded.type !== 'access') {
 throw new Error('Invalid token type');
}
```

#### 3. Refresh Token Rotation Missing (Medium)

**Issue:** Refresh token reused until expiry.

**Attack:** Stolen refresh token grants indefinite access.

**Fix:** Rotate on use:
```javascript
async function refreshAccessToken(refreshToken) {
 const payload = jwt.verify(refreshToken, secret);
 
 // Check if token is in Redis (not already used)
 const exists = await redis.get(`refresh:${refreshToken}`);
 if (!exists) throw new Error('Token revoked or already used');
 
 // Delete old token
 await redis.del(`refresh:${refreshToken}`);
 
 // Issue new refresh token
 const newRefreshToken = generateRefreshToken(payload.userId);
 await redis.setex(`refresh:${newRefreshToken}`, 7 * 24 * 60 * 60, payload.userId);
 
 return {
 accessToken: generateAccessToken(payload.userId),
 refreshToken: newRefreshToken
 };
}
```

#### 4. No Refresh Token Family Tracking (Medium)

**Issue:** If attacker uses stolen refresh token before user, user's next refresh fails but attacker continues.

**Fix:** Track token families and revoke entire family on reuse detection.

### Security Checklist

| Control | Status |
|---------|--------|
| Passwords hashed (bcrypt) | ✅ |
| httpOnly cookie for refresh | ✅ |
| Secure cookie flag | ⚠️ Check production |
| SameSite cookie attribute | ❌ Add SameSite=Strict |
| Token expiration | ✅ |
| Token revocation | ✅ Via Redis |
| Rate limiting on login | ❌ Add |
| Account lockout | ❌ Add |

When implementing or reviewing authentication systems, token handling, or session management

  • Authentication is too important to get wrong, consider using established libraries (Passport, NextAuth)
  • Always have a way to revoke tokens, stateless JWT alone can't handle compromise

4. Security Headers Configuration

Help me configure security headers for my application:

**Application type:** [WEB APP / API / STATIC SITE]
**Framework:** [EXPRESS / NGINX / CLOUDFLARE / etc.]

**Content served:**
- [HTML / JSON API / BOTH]
- [USES IFRAMES? / LOADS EXTERNAL SCRIPTS?]

**Current headers (if any):**
```
[PASTE CURRENT HEADERS]
```

**Requirements:**
- [COMPLIANCE NEEDS - PCI, SOC2, etc.]
- [SPECIFIC CONCERNS - XSS, clickjacking]

Provide:
1. Recommended headers with values
2. Explanation of what each prevents
3. Implementation code/config
4. Testing instructions
Application: React SPA + REST API
Framework: Express.js backend, Nginx reverse proxy
Content: HTML app at /, JSON API at /api/*
External content: Google Fonts, Stripe.js
Compliance: SOC2
## Security Headers Configuration

### Express.js Middleware (using Helmet)

```javascript
const helmet = require('helmet');

app.use(helmet({
 // Content Security Policy - prevents XSS
 contentSecurityPolicy: {
 directives: {
 defaultSrc: ["'self'"],
 scriptSrc: [
 "'self'",
 "https://js.stripe.com", // Stripe
 // Avoid 'unsafe-inline' if possible
 ],
 styleSrc: [
 "'self'",
 "https://fonts.googleapis.com",
 "'unsafe-inline'", // Often needed for CSS-in-JS
 ],
 fontSrc: [
 "'self'",
 "https://fonts.gstatic.com",
 ],
 imgSrc: ["'self'", "data:", "https:"],
 connectSrc: [
 "'self'",
 "https://api.stripe.com",
 ],
 frameSrc: [
 "https://js.stripe.com", // Stripe iframe
 ],
 frameAncestors: ["'none'"], // Prevent clickjacking
 objectSrc: ["'none'"],
 upgradeInsecureRequests: [],
 },
 },
 
 // Prevent MIME sniffing
 noSniff: true,
 
 // XSS filter (legacy browsers)
 xssFilter: true,
 
 // Prevent clickjacking
 frameguard: { action: 'deny' },
 
 // HSTS - force HTTPS
 hsts: {
 maxAge: 31536000, // 1 year
 includeSubDomains: true,
 preload: true,
 },
 
 // Hide Express
 hidePoweredBy: true,
 
 // Referrer policy
 referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

// Additional headers for API routes
app.use('/api', (req, res, next) => {
 res.setHeader('Content-Type', 'application/json');
 res.setHeader('Cache-Control', 'no-store'); // Don't cache API responses
 next();
});
```

### Nginx Configuration

```nginx
server {
 #... SSL config...

 # Security headers
 add_header X-Frame-Options "DENY" always;
 add_header X-Content-Type-Options "nosniff" always;
 add_header X-XSS-Protection "1; mode=block" always;
 add_header Referrer-Policy "strict-origin-when-cross-origin" always;
 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
 
 # CSP (if not set by app)
 add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com;..." always;
 
 # Permissions Policy (formerly Feature-Policy)
 add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
```

### Header Explanations

| Header | Value | Prevents |
|--------|-------|----------|
| Content-Security-Policy | Allowlist | XSS, data injection |
| X-Frame-Options | DENY | Clickjacking |
| X-Content-Type-Options | nosniff | MIME-type attacks |
| Strict-Transport-Security | max-age=... | Protocol downgrade |
| Referrer-Policy | strict-origin | URL leakage |
| Permissions-Policy | deny features | Unwanted API access |

### Testing

**Online tools:**
- https://securityheaders.com - Grade your headers
- https://observatory.mozilla.org - Mozilla's scanner

**Manual check:**
```bash
curl -I https://yoursite.com
```

**Expected grade:** A or A+ on securityheaders.com

### SOC2 Compliance Notes

✅ Required for SOC2:
- HSTS with 1-year max-age
- Content-Security-Policy
- X-Frame-Options or CSP frame-ancestors

⚠️ Document any CSP exceptions (unsafe-inline) with justification

When deploying web applications, hardening existing apps, or meeting compliance requirements

  • Start with a strict CSP and loosen only when needed, it's easier than tightening later
  • Use CSP report-uri to monitor violations before enforcing

Common Mistakes to Avoid

Focusing only on OWASP Top 10 while missing business logic flaws specific to your application

Assuming frameworks handle security automatically, most require explicit configuration

Testing only happy paths, attackers send malformed, oversized, and unexpected inputs

Frequently Asked Questions

Security vulnerabilities often hide in plain sight, injection flaws, authentication bypasses, and data exposure that seem obvious in retrospect. These prompts help you think like an attacker to find vulnerabilities before they're exploited. They're for defensive purposes: identifying weaknesses in your own code and implementing secure patterns.

Related Templates

Have your own prompt to optimize?