Глава 18. Безопасность кода в Cursor
18.1. Выявление уязвимостей
Автоматический security audit с Cursor
You: Проанализируй этот код на security уязвимости
[Вставьте код]
AI:Пример анализа:
// Исходный код с уязвимостями
import express from 'express';
import mysql from 'mysql';
const app = express();
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password123',
database: 'myapp'
});
app.get('/user/:id', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
db.query(query, (err, results) => {
res.json(results);
});
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query, (err, results) => {
if (results.length > 0) {
res.json({ token: 'fake-jwt-token' });
}
});
});
AI анализ и рекомендации:
КРИТИЧЕСКИЕ УЯЗВИМОСТИ:
1. SQL Injection (Severity: CRITICAL)
Lines: 14, 22
Issue: Прямая конкатенация user input в SQL queries
Vulnerable code:
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
Attack example:
GET /user/1 OR 1=1-- (вернёт всех пользователей)
GET /user/1; DROP TABLE users-- (удалит таблицу)
FIX:
Use parameterized queries:
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [req.params.id], (err, results) => { ... });
2. Password stored in plaintext (Severity: CRITICAL)
Line: 22
Issue: Пароли хранятся и сравниваются в открытом виде
FIX:
Use bcrypt for password hashing:
// On registration
const hashedPassword = await bcrypt.hash(password, 12);
// On login
const isValid = await bcrypt.compare(password, user.password_hash);
3. Hardcoded credentials (Severity: HIGH)
Lines: 6-9
Issue: Database credentials в коде
FIX:
Use environment variables:
const db = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
СЕРЬЁЗНЫЕ ПРОБЛЕМЫ:
4. No authentication on sensitive endpoint (Severity: HIGH)
Line: 13
Issue: Endpoint выдаёт user data без аутентификации
FIX:
Add authentication middleware:
app.get('/user/:id', authenticateToken, (req, res) => { ... });
5. Information disclosure (Severity: MEDIUM)
Line: 16
Issue: Возвращается полная информация о пользователе
FIX:
Exclude sensitive fields:
const { password, salt, ...safeUser } = results[0];
res.json(safeUser);
6. Missing rate limiting (Severity: MEDIUM)
Issue: No rate limiting на login endpoint
FIX:
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5
});
app.post('/login', loginLimiter, ...);
ИСПРАВЛЕННАЯ ВЕРСИЯ:// Безопасная версия
import express from 'express';
import mysql from 'mysql2/promise';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { body, param, validationResult } from 'express-validator';
const app = express();
// Security middleware
app.use(helmet());
app.use(express.json({ limit: '10mb' }));
// Database connection with environment variables
const db = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Rate limiting
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later'
});
// Authentication middleware
async function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}
// Secure endpoints
app.get(
'/user/:id',
authenticateToken,
param('id').isUUID(),
async (req, res) => {
// Validate input
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
// Parameterized query
const [rows] = await db.query(
'SELECT id, email, username, first_name, last_name, created_at FROM users WHERE id = ?',
[req.params.id]
);
if (rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
// Check authorization (user can only view own data or admin)
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json({ success: true, data: rows[0] });
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
app.post(
'/login',
loginLimiter,
body('username').isLength({ min: 3, max: 50 }).trim().escape(),
body('password').isLength({ min: 8 }),
async (req, res) => {
// Validate input
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, password } = req.body;
try {
// Parameterized query
const [rows] = await db.query(
'SELECT id, username, password_hash, email FROM users WHERE username = ?',
[username]
);
if (rows.length === 0) {
// Generic error to prevent username enumeration
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = rows[0];
// Compare hashed password
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
// Log failed attempt
await db.query(
'INSERT INTO login_attempts (user_id, success, ip_address) VALUES (?, ?, ?)',
[user.id, false, req.ip]
);
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT
const token = jwt.sign(
{ id: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Log successful login
await db.query(
'INSERT INTO login_attempts (user_id, success, ip_address) VALUES (?, ?, ?)',
[user.id, true, req.ip]
);
res.json({
success: true,
token,
expiresIn: 3600
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
18.2. Secure coding practices
Security Rules для Cursor
# .cursor/security-rules.mdc
# Security-First Development Rules
## Input Validation
ALWAYS validate and sanitize all user inputs:
```typescript
// GOOD
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150)
});
app.post('/users', (req, res) => {
const validated = userSchema.parse(req.body);
// Use validated data
});
// BAD
app.post('/users', (req, res) => {
const email = req.body.email; // No validation
// Direct use without validation
});
SQL Injection Prevention
NEVER concatenate user input in SQL queries:
// GOOD - Parameterized queries
const user = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// BAD - String concatenation
const user = await db.query(
`SELECT * FROM users WHERE email = '${email}'`
);
XSS Prevention
Always escape user-generated content:
// GOOD
import DOMPurify from 'dompurify';
const sanitized = DOMPurify.sanitize(userInput);
element.innerHTML = sanitized;
// BAD
element.innerHTML = userInput; // Direct HTML injection
Authentication & Authorization
// GOOD - JWT with expiration
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Verify on protected routes
const authenticateToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.sendStatus(401);
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
req.user = user;
next();
} catch {
return res.sendStatus(403);
}
};
// BAD - No expiration, no verification
const token = Buffer.from(JSON.stringify(user)).toString('base64');
Password Security
// GOOD - Bcrypt with high cost factor
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return await bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// BAD - Plain text or weak hashing
const hash = crypto.createHash('md5').update(password).digest('hex');
Secrets Management
// GOOD - Environment variables
const apiKey = process.env.API_KEY;
const dbPassword = process.env.DB_PASSWORD;
// BAD - Hardcoded secrets
const apiKey = 'sk-1234567890abcdef';
const dbPassword = 'mypassword123';
CORS Configuration
// GOOD - Specific origins
app.use(cors({
origin: ['https://example.com', 'https://www.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// BAD - Wildcard origin with credentials
app.use(cors({
origin: '*',
credentials: true
}));
File Upload Security
// GOOD - Validation and sanitization
import multer from 'multer';
import path from 'path';
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|pdf/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
// BAD - No validation
const upload = multer({ dest: 'uploads/' });
API Rate Limiting
// GOOD - Rate limiting per endpoint
import rateLimit from 'express-rate-limit';
const createAccountLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 accounts per hour per IP
message: 'Too many accounts created from this IP'
});
app.post('/register', createAccountLimiter, ...);
// Different limits for different endpoints
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', apiLimiter);
Error Handling
// GOOD - Generic error messages
try {
const user = await findUserByEmail(email);
if (!user || !await verifyPassword(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
} catch (error) {
logger.error('Login error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
// BAD - Detailed error messages
catch (error) {
return res.status(500).json({
error: error.message, // Exposes implementation details
stack: error.stack // Exposes code structure
});
}
CSRF Protection
// GOOD - CSRF tokens
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// Process form
});
Security Headers
// GOOD - Comprehensive security headers
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
18.3. Анализ зависимостей
Automated dependency scanning
You: Создай скрипт для проверки уязвимостей в зависимостях
AI:// scripts/security-audit.js
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
class DependencyAuditor {
constructor() {
this.vulnerabilities = [];
this.outdatedPackages = [];
}
async runNpmAudit() {
return new Promise((resolve, reject) => {
exec('npm audit --json', (error, stdout, stderr) => {
if (stdout) {
try {
const result = JSON.parse(stdout);
this.vulnerabilities = this.parseNpmAudit(result);
resolve(this.vulnerabilities);
} catch (e) {
reject(e);
}
} else if (error) {
// npm audit exits with error code if vulnerabilities found
resolve([]);
}
});
});
}
parseNpmAudit(auditResult) {
const vulnerabilities = [];
if (auditResult.vulnerabilities) {
for (const [packageName, vuln] of Object.entries(auditResult.vulnerabilities)) {
vulnerabilities.push({
package: packageName,
severity: vuln.severity,
title: vuln.via[0]?.title || 'Unknown',
url: vuln.via[0]?.url,
range: vuln.range,
fixAvailable: vuln.fixAvailable
});
}
}
return vulnerabilities;
}
async checkOutdated() {
return new Promise((resolve, reject) => {
exec('npm outdated --json', (error, stdout, stderr) => {
if (stdout) {
try {
const result = JSON.parse(stdout);
this.outdatedPackages = Object.entries(result).map(([name, info]) => ({
package: name,
current: info.current,
wanted: info.wanted,
latest: info.latest,
type: info.type
}));
resolve(this.outdatedPackages);
} catch (e) {
reject(e);
}
} else {
resolve([]);
}
});
});
}
async runSnykTest() {
// Requires Snyk CLI: npm install -g snyk
return new Promise((resolve, reject) => {
exec('snyk test --json', (error, stdout, stderr) => {
if (stdout) {
try {
const result = JSON.parse(stdout);
resolve(result);
} catch (e) {
reject(e);
}
} else {
resolve(null);
}
});
});
}
generateReport() {
const report = {
timestamp: new Date().toISOString(),
summary: {
totalVulnerabilities: this.vulnerabilities.length,
critical: this.vulnerabilities.filter(v => v.severity === 'critical').length,
high: this.vulnerabilities.filter(v => v.severity === 'high').length,
moderate: this.vulnerabilities.filter(v => v.severity === 'moderate').length,
low: this.vulnerabilities.filter(v => v.severity === 'low').length,
outdatedPackages: this.outdatedPackages.length
},
vulnerabilities: this.vulnerabilities,
outdatedPackages: this.outdatedPackages
};
return report;
}
printReport() {
const report = this.generateReport();
console.log('\n' + '='.repeat(70));
console.log('SECURITY AUDIT REPORT');
console.log('='.repeat(70));
console.log('\nSUMMARY:');
console.log(` Total Vulnerabilities: ${report.summary.totalVulnerabilities}`);
console.log(` Critical: ${report.summary.critical}`);
console.log(` High: ${report.summary.high}`);
console.log(` Moderate: ${report.summary.moderate}`);
console.log(` Low: ${report.summary.low}`);
console.log(` Outdated Packages: ${report.summary.outdatedPackages}`);
if (this.vulnerabilities.length > 0) {
console.log('\nVULNERABILITIES:');
console.log('-'.repeat(70));
const grouped = this.groupBySeverity(this.vulnerabilities);
for (const severity of ['critical', 'high', 'moderate', 'low']) {
if (grouped[severity].length > 0) {
console.log(`\n${severity.toUpperCase()}:`);
grouped[severity].forEach(v => {
console.log(` • ${v.package}`);
console.log(` ${v.title}`);
if (v.fixAvailable) {
console.log(` Fix: npm audit fix`);
}
});
}
}
}
if (this.outdatedPackages.length > 0) {
console.log('\nOUTDATED PACKAGES:');
console.log('-'.repeat(70));
this.outdatedPackages.forEach(pkg => {
console.log(` ${pkg.package}: ${pkg.current} → ${pkg.latest}`);
});
}
console.log('\n' + '='.repeat(70));
// Save report
fs.writeFileSync(
'security-report.json',
JSON.stringify(report, null, 2)
);
console.log('Report saved to: security-report.json\n');
// Exit with error if critical vulnerabilities found
if (report.summary.critical > 0) {
process.exit(1);
}
}
groupBySeverity(vulnerabilities) {
return vulnerabilities.reduce((acc, vuln) => {
if (!acc[vuln.severity]) {
acc[vuln.severity] = [];
}
acc[vuln.severity].push(vuln);
return acc;
}, {});
}
}
// Run audit
async function main() {
const auditor = new DependencyAuditor();
console.log('Running security audit...\n');
await auditor.runNpmAudit();
await auditor.checkOutdated();
auditor.printReport();
}
main().catch(console.error);
# .github/workflows/security-scan.yml
name: Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly on Sundays
jobs:
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: |
npm audit --audit-level=moderate
node scripts/security-audit.js
continue-on-error: true
- name: Run Snyk test
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Upload security report
uses: actions/upload-artifact@v3
with:
name: security-report
path: security-report.json
- name: Comment PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('security-report.json'));
const comment = `
## Security Audit Results
**Summary:**
- Critical: ${report.summary.critical}
- High: ${report.summary.high}
- Moderate: ${report.summary.moderate}
- Low: ${report.summary.low}
- Outdated packages: ${report.summary.outdatedPackages}
${report.summary.critical > 0 ? ' **Critical vulnerabilities found!**' : 'No critical vulnerabilities'}
Full report available in artifacts.
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
18.4. Secrets management
Безопасное управление секретами
You: Создай систему для безопасного управления секретами
AI:// utils/secrets-manager.ts
import * as AWS from 'aws-sdk';
import { promisify } from 'util';
import * as crypto from 'crypto';
/**
* Secrets Manager для безопасного хранения и получения секретов
* Поддерживает AWS Secrets Manager, HashiCorp Vault, или локальное шифрование
*/
export class SecretsManager {
private provider: 'aws' | 'vault' | 'local';
private secretsCache: Map<string, { value: string; expiry: number }>;
private cacheTTL: number = 300000; // 5 minutes
constructor(provider: 'aws' | 'vault' | 'local' = 'aws') {
this.provider = provider;
this.secretsCache = new Map();
}
/**
* Получить секрет по имени
*/
async getSecret(secretName: string): Promise<string> {
// Check cache first
const cached = this.secretsCache.get(secretName);
if (cached && cached.expiry > Date.now()) {
return cached.value;
}
let secretValue: string;
switch (this.provider) {
case 'aws':
secretValue = await this.getFromAWS(secretName);
break;
case 'vault':
secretValue = await this.getFromVault(secretName);
break;
case 'local':
secretValue = await this.getFromLocal(secretName);
break;
}
// Cache secret
this.secretsCache.set(secretName, {
value: secretValue,
expiry: Date.now() + this.cacheTTL
});
return secretValue;
}
/**
* Сохранить секрет
*/
async setSecret(secretName: string, secretValue: string): Promise<void> {
switch (this.provider) {
case 'aws':
await this.saveToAWS(secretName, secretValue);
break;
case 'vault':
await this.saveToVault(secretName, secretValue);
break;
case 'local':
await this.saveToLocal(secretName, secretValue);
break;
}
// Invalidate cache
this.secretsCache.delete(secretName);
}
/**
* Ротация секрета
*/
async rotateSecret(secretName: string, generateNewValue: () => string): Promise<void> {
const newValue = generateNewValue();
await this.setSecret(secretName, newValue);
logger.info(`Secret rotated: ${secretName}`);
}
private async getFromAWS(secretName: string): Promise<string> {
const client = new AWS.SecretsManager({
region: process.env.AWS_REGION
});
const data = await client.getSecretValue({ SecretId: secretName }).promise();
if ('SecretString' in data) {
return data.SecretString!;
} else {
const buff = Buffer.from(data.SecretBinary as string, 'base64');
return buff.toString('ascii');
}
}
private async saveToAWS(secretName: string, secretValue: string): Promise<void> {
const client = new AWS.SecretsManager({
region: process.env.AWS_REGION
});
try {
await client.createSecret({
Name: secretName,
SecretString: secretValue
}).promise();
} catch (error: any) {
if (error.code === 'ResourceExistsException') {
await client.updateSecret({
SecretId: secretName,
SecretString: secretValue
}).promise();
} else {
throw error;
}
}
}
private async getFromVault(secretName: string): Promise<string> {
// HashiCorp Vault implementation
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
const result = await vault.read(`secret/data/${secretName}`);
return result.data.data.value;
}
private async saveToVault(secretName: string, secretValue: string): Promise<void> {
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
await vault.write(`secret/data/${secretName}`, {
data: { value: secretValue }
});
}
private async getFromLocal(secretName: string): Promise<string> {
const secretsPath = path.join(process.cwd(), '.secrets.enc');
if (!fs.existsSync(secretsPath)) {
throw new Error('Secrets file not found');
}
const encrypted = fs.readFileSync(secretsPath, 'utf8');
const decrypted = this.decrypt(encrypted);
const secrets = JSON.parse(decrypted);
if (!secrets[secretName]) {
throw new Error(`Secret not found: ${secretName}`);
}
return secrets[secretName];
}
private async saveToLocal(secretName: string, secretValue: string): Promise<void> {
const secretsPath = path.join(process.cwd(), '.secrets.enc');
let secrets = {};
if (fs.existsSync(secretsPath)) {
const encrypted = fs.readFileSync(secretsPath, 'utf8');
const decrypted = this.decrypt(encrypted);
secrets = JSON.parse(decrypted);
}
secrets[secretName] = secretValue;
const encrypted = this.encrypt(JSON.stringify(secrets));
fs.writeFileSync(secretsPath, encrypted);
}
private encrypt(text: string): string {
const algorithm = 'aes-256-gcm';
const key = this.getEncryptionKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return JSON.stringify({
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
});
}
private decrypt(encryptedData: string): string {
const algorithm = 'aes-256-gcm';
const key = this.getEncryptionKey();
const { encrypted, iv, authTag } = JSON.parse(encryptedData);
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
private getEncryptionKey(): Buffer {
const key = process.env.SECRETS_ENCRYPTION_KEY;
if (!key) {
throw new Error('SECRETS_ENCRYPTION_KEY environment variable not set');
}
return crypto.scryptSync(key, 'salt', 32);
}
}
// Export singleton
export const secretsManager = new SecretsManager(
process.env.SECRETS_PROVIDER as any || 'local'
);
// Usage examples:
// const dbPassword = await secretsManager.getSecret('database-password');
// const apiKey = await secretsManager.getSecret('stripe-api-key');
Pre-commit hook для проверки секретов:
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit security checks..."
# Check for secrets
if git diff --cached --name-only | xargs grep -E "(api[_-]?key|password|secret|token)\s*=\s*['\"]" > /dev/null 2>&1; then
echo " Potential secrets detected in staged files!"
echo "Please review and remove hardcoded credentials."
echo ""
echo "Detected patterns:"
git diff --cached | grep -E "(api[_-]?key|password|secret|token)\s*=\s*['\"]"
echo ""
echo "To bypass this check (NOT RECOMMENDED):"
echo " git commit --no-verify"
exit 1
fi
# Check for .env files
if git diff --cached --name-only | grep -E "\.env$" > /dev/null 2>&1; then
echo " .env file in staged changes!"
echo "Never commit .env files to version control."
exit 1
fi
# Check file sizes (prevent large binaries)
MAX_SIZE=1048576 # 1MB
while read file; do
size=$(git cat-file -s ":0:$file")
if [ $size -gt $MAX_SIZE ]; then
echo "File too large: $file ($(numfmt --to=iec $size))"
echo "Maximum allowed: $(numfmt --to=iec $MAX_SIZE)"
exit 1
fi
done < <(git diff --cached --name-only --diff-filter=ACM)
echo "Pre-commit checks passed"
exit 0
18.5. Аудит безопасности
Automated security testing
You: Создай автоматизированные security тесты
AI:// tests/security.test.ts
import request from 'supertest';
import app from '../app';
import { db } from '../database';
describe('Security Tests', () => {
describe('SQL Injection Prevention', () => {
it('should prevent SQL injection in login', async () => {
const maliciousInput = "admin' OR '1'='1";
const response = await request(app)
.post('/api/auth/login')
.send({
username: maliciousInput,
password: 'password'
});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid credentials');
// Verify no user was authenticated
expect(response.body.token).toBeUndefined();
});
it('should prevent SQL injection in user lookup', async () => {
const maliciousId = "1 OR 1=1--";
const response = await request(app)
.get(`/api/users/${maliciousId}`)
.set('Authorization', 'Bearer valid-token');
expect(response.status).not.toBe(200);
});
});
describe('XSS Prevention', () => {
it('should sanitize HTML in user input', async () => {
const xssPayload = '<script>alert("XSS")</script>';
const response = await request(app)
.post('/api/comments')
.set('Authorization', 'Bearer valid-token')
.send({
content: xssPayload
});
expect(response.body.data.content).not.toContain('<script>');
expect(response.body.data.content).not.toContain('alert');
});
});
describe('Authentication Security', () => {
it('should reject weak passwords', async () => {
const weakPasswords = [
'password',
'12345678',
'qwerty',
'abc123'
];
for (const password of weakPasswords) {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
username: 'testuser',
password
});
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/password/i);
}
});
it('should enforce rate limiting on login', async () => {
const attempts = Array(10).fill(null);
const responses = await Promise.all(
attempts.map(() =>
request(app)
.post('/api/auth/login')
.send({
username: 'user',
password: 'wrong'
})
)
);
// After 5 attempts, should get rate limited
const rateLimited = responses.filter(r => r.status === 429);
expect(rateLimited.length).toBeGreaterThan(0);
});
it('should invalidate old tokens after password change', async () => {
// Login and get token
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
username: 'testuser',
password: 'oldpassword'
});
const oldToken = loginResponse.body.token;
// Change password
await request(app)
.put('/api/users/me/password')
.set('Authorization', `Bearer ${oldToken}`)
.send({
currentPassword: 'oldpassword',
newPassword: 'NewSecurePassword123!'
});
// Old token should be invalid
const response = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${oldToken}`);
expect(response.status).toBe(401);
});
});
describe('Authorization', () => {
it('should prevent users from accessing other users data', async () => {
// User 1 token
const user1Token = 'token-for-user-1';
// Try to access user 2 data
const response = await request(app)
.get('/api/users/user-2-id')
.set('Authorization', `Bearer ${user1Token}`);
expect(response.status).toBe(403);
});
it('should prevent privilege escalation', async () => {
const userToken = 'regular-user-token';
// Try to access admin endpoint
const response = await request(app)
.get('/api/admin/users')
.set('Authorization', `Bearer ${userToken}`);
expect(response.status).toBe(403);
});
});
describe('CSRF Protection', () => {
it('should reject requests without CSRF token', async () => {
const response = await request(app)
.post('/api/users/me')
.set('Authorization', 'Bearer valid-token')
.send({ name: 'New Name' });
// Without CSRF token
expect(response.status).toBe(403);
});
it('should accept requests with valid CSRF token', async () => {
// Get CSRF token
const tokenResponse = await request(app)
.get('/api/csrf-token')
.set('Authorization', 'Bearer valid-token');
const csrfToken = tokenResponse.body.csrfToken;
// Make request with CSRF token
const response = await request(app)
.post('/api/users/me')
.set('Authorization', 'Bearer valid-token')
.set('X-CSRF-Token', csrfToken)
.send({ name: 'New Name' });
expect(response.status).toBe(200);
});
});
describe('Input Validation', () => {
it('should reject invalid email formats', async () => {
const invalidEmails = [
'notanemail',
'@example.com',
'user@',
'user @example.com'
];
for (const email of invalidEmails) {
const response = await request(app)
.post('/api/auth/register')
.send({
email,
username: 'user',
password: 'ValidPass123!'
});
expect(response.status).toBe(400);
}
});
it('should sanitize file names in uploads', async () => {
const maliciousFileName = '../../../etc/passwd';
const response = await request(app)
.post('/api/upload')
.set('Authorization', 'Bearer valid-token')
.attach('file', Buffer.from('test'), maliciousFileName);
// Filename should be sanitized
expect(response.body.filename).not.toContain('..');
expect(response.body.filename).not.toContain('/');
});
});
describe('Security Headers', () => {
it('should include security headers', async () => {
const response = await request(app).get('/');
expect(response.headers['x-frame-options']).toBeDefined();
expect(response.headers['x-content-type-options']).toBe('nosniff');
expect(response.headers['x-xss-protection']).toBeDefined();
expect(response.headers['strict-transport-security']).toBeDefined();
});
it('should have CSP header', async () => {
const response = await request(app).get('/');
expect(response.headers['content-security-policy']).toBeDefined();
expect(response.headers['content-security-policy']).toContain("default-src 'self'");
});
});
describe('Session Security', () => {
it('should use secure cookies in production', async () => {
process.env.NODE_ENV = 'production';
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'testuser',
password: 'password'
});
const setCookie = response.headers['set-cookie'];
expect(setCookie).toBeDefined();
expect(setCookie[0]).toMatch(/Secure/);
expect(setCookie[0]).toMatch(/HttpOnly/);
expect(setCookie[0]).toMatch(/SameSite/);
});
it('should generate unique session IDs', async () => {
const sessions = await Promise.all(
Array(100).fill(null).map(() =>
request(app)
.post('/api/auth/login')
.send({
username: 'user',
password: 'password'
})
)
);
const tokens = sessions.map(s => s.body.token);
const uniqueTokens = new Set(tokens);
expect(uniqueTokens.size).toBe(100);
});
});
});