Безопасность кода в Cursor | Глава 18

18 октября 2025
97

Глава 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);
});
});
});