Instruction file imported from movahedan/fivenines (
.cursor/rules/security.mdc). Copyright stays with the author.
Security
ReDoS Prevention
// ❌ VULNERABLE - Catastrophic backtracking
/.*#(\d+).*/ // Exponential runtime
/^(\w+)(?:\(([^)]+)\))?(!)?:\s*([^\r\n]+?)\s*$/ // Multiple backtracking issues
/.*\d+.*/ // Greedy quantifiers with overlapping patterns
/(a+)+/ // Nested quantifiers (evil regex)
/(a|aa)*/ // Exponential backtracking
/.*#\d+.*/ // Can match anywhere, causes backtracking
// ✅ SECURE - Linear runtime guaranteed
/#(\d+)/ // Direct match, no backtracking
/\d+/ // Simple, direct match
/a+/ // Single quantifier
/(?:a|aa)*/ // Non-capturing, optimized
/\d{1,10}/ // Bounded range
/^#\d+$/ // Anchored to start/end
// ❌ VULNERABLE - alternation with repeated quantifiers (super-linear backtracking)
/^-+|-+$/g // trim hyphens — use a linear scan instead
// ✅ SECURE - trim leading/trailing separators without regex
function trimEdgeHyphens(value: string): string {
let start = 0;
let end = value.length;
while (start < end && value[start] === "-") start++;
while (end > start && value[end - 1] === "-") end--;
return value.slice(start, end);
}
For user-controlled strings (emails, names, slugs), prefer a single-pass character loop over chained .replace(/…/g) when normalizing or stripping edge characters. Cap input length first (for example email local-part ≤ 64).
User-supplied patterns (CLI, config, API)
new RegExp(userControlledString) and then running .test / .match across many paths or rows is a common ReDoS surface. Before any high-fan-out matching:
- Cap pattern size (character limit) and cap each candidate string length so amplification stays bounded.
- Screen the pattern with a catastrophic-backtracking heuristic (for example the
safe-regexpackage used in repo scripts) and reject unsafe patterns with a clear error. Heuristics can false-positive or false-negative; pair them with length limits and simple patterns when possible. - Prefer linear-time matchers (RE2, or constrained glob-to-regex) when the product allows it.
Promise.race + pattern.test does not cancel work: in typical JavaScript runtimes, RegExp.prototype.test runs synchronously on the calling thread until it returns, so a timeout in another microtask does not interrupt catastrophic backtracking. Use static checks, bounded inputs, a separate worker you can terminate, or a safe engine—not a fake “async timeout” around sync .test.
Input Validation
// ✅ Validate and sanitize
const validateEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const validatePassword = (password: string): boolean => {
// At least 8 chars, 1 uppercase, 1 lowercase, 1 number
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/.test(password);
};
const sanitizeHtml = (html: string): string =>
html
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
// ❌ Bad - no length check (DoS risk)
const processInput = (input: string): string => input.replace(/[^\w\s#]/g, '');
// ✅ Good - validate length first
const processInput = (input: string): string => {
if (input.length > 1000) throw new Error('Input too long');
return input.replace(/[^\w\s#]/g, '');
};
Environment Variables
// ❌ Bad - no validation
const API_KEY = process.env.API_KEY;
const DATABASE_URL = process.env.DATABASE_URL;
// ✅ Good - validate required env vars
const API_KEY = process.env.API_KEY;
if (!API_KEY) throw new Error('API_KEY is required');
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) throw new Error('DATABASE_URL is required');
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) throw new Error('JWT_SECRET is required');
Authentication
// ❌ Bad - no error handling
const authenticate = (token: string) => jwt.verify(token, process.env.JWT_SECRET);
// ✅ Good - JWT with proper error handling
const authenticate = async (token: string) => {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new Error('Invalid token');
}
};
// ✅ Token refresh
const refreshToken = async (refreshToken: string) => {
try {
const response = await api.post('/auth/refresh', { refreshToken });
return response.data.accessToken;
} catch (error) {
throw new Error('Token refresh failed');
}
};
// ⚠️ Misleading — Promise.race does not stop a running sync .test(); see "User-supplied patterns" above
const unsafeTimeoutAroundSyncTest = async (pattern: RegExp, input: string): Promise<boolean> => {
return await Promise.race([
Promise.resolve(pattern.test(input)),
new Promise<boolean>((_, reject) => setTimeout(() => reject(new Error('Regex timeout')), 1000))
]);
};
SQL Injection Prevention
// ❌ Bad - vulnerable to SQL injection
const badQuery = `SELECT * FROM users WHERE id = ${userId}`;
const badQuery2 = `SELECT * FROM users WHERE name = '${userName}'`;
// ✅ Good - parameterized queries
const getUserById = async (id: number) => {
const query = 'SELECT * FROM users WHERE id = ?';
const result = await db.execute(query, [id]);
return result[0];
};
const getUserByName = async (name: string) => {
const query = 'SELECT * FROM users WHERE name = ?';
const result = await db.execute(query, [name]);
return result[0];
};
Regex Security Patterns
// ❌ Bad - alternation + quantifiers on user input (tenant slug, slugify)
emailLocal.replace(/^-+|-+$/g, "");
// ✅ Good - linear scan after length cap
function slugifyLocalPart(local: string): string { /* char loop, no backtracking */ }
// ❌ Bad - greedy quantifier with overlapping patterns
const badPattern = /.*#\d+.*/;
// ✅ Good - direct match
const goodPattern = /#\d+/;
// ❌ Bad - capturing group overhead
const badPattern = /(\w+)(\([^)]+\))/;
// ✅ Good - non-capturing group
const goodPattern = /(\w+)(?:\([^)]+\))/;
// ❌ Bad - unbounded range
const badPattern = /\d+/;
// ✅ Good - bounded range
const goodPattern = /\d{1,10}/;
// ❌ Bad - can match anywhere in string
const badPattern = /#\d+/;
// ✅ Good - anchored to start/end
const goodPattern = /^#\d+$/;
Rate Limiting & CORS
// ❌ Bad - no rate limiting
app.use('/api/', (req, res, next) => next());
// ✅ Good - rate limiting
const rateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', rateLimiter);
// ❌ Bad - permissive CORS
app.use(cors());
// ✅ Good - configured CORS
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Security Checklist
- No nested quantifiers like
(a+)+ - No greedy + lazy combinations like
.*+? - No overlapping patterns that can backtrack
- Use bounded quantifiers when possible
- Test with malicious input (repeated characters)
- Input length limits to prevent DoS
- User-supplied regex: length caps, heuristic ReDoS screen (or safe engine), bounded strings under test
- Do not rely on
Promise.racealone to cap syncRegExpruntime in JavaScript - Validate and sanitize input before processing
- All sensitive data in environment variables
- Proper CORS configuration
- Rate limiting implemented
- SQL injection prevention (parameterized queries)
- XSS protection in place