Skip to main content

Лучшие практики безопасности

Комплексное руководство по обеспечению безопасности платформы маркетплейсов.

Аутентификация и авторизация

Многофакторная аутентификация (MFA)

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// Включение 2FA для пользователя
app.post('/api/auth/enable-2fa', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;

// Генерируем секретный ключ
const secret = speakeasy.generateSecret({
name: `Marketplace (${req.user.email})`,
issuer: 'Marketplace Platform'
});

// Сохраняем временный секрет
await db.query(
'UPDATE users SET temp_2fa_secret = $1 WHERE id = $2',
[secret.base32, userId]
);

// Генерируем QR код
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

res.json({
secret: secret.base32,
qr_code: qrCodeUrl,
manual_entry_key: secret.base32
});

} catch (error) {
console.error('2FA setup error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// Подтверждение 2FA
app.post('/api/auth/verify-2fa', authenticateToken, async (req, res) => {
try {
const { token } = req.body;
const userId = req.user.id;

const user = await db.query(
'SELECT temp_2fa_secret FROM users WHERE id = $1',
[userId]
);

if (!user.rows[0].temp_2fa_secret) {
return res.status(400).json({ error: '2FA setup not initiated' });
}

// Проверяем токен
const verified = speakeasy.totp.verify({
secret: user.rows[0].temp_2fa_secret,
encoding: 'base32',
token: token,
window: 2
});

if (!verified) {
return res.status(400).json({ error: 'Invalid 2FA token' });
}

// Активируем 2FA
await db.query(
'UPDATE users SET two_factor_secret = temp_2fa_secret, temp_2fa_secret = NULL, two_factor_enabled = true WHERE id = $1',
[userId]
);

res.json({ message: '2FA enabled successfully' });

} catch (error) {
console.error('2FA verification error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// Проверка 2FA при входе
function verify2FA(req, res, next) {
const { two_factor_token } = req.body;

if (req.user.two_factor_enabled) {
if (!two_factor_token) {
return res.status(400).json({
error: '2FA token required',
requires_2fa: true
});
}

const verified = speakeasy.totp.verify({
secret: req.user.two_factor_secret,
encoding: 'base32',
token: two_factor_token,
window: 2
});

if (!verified) {
return res.status(400).json({ error: 'Invalid 2FA token' });
}
}

next();
}

JWT токены с rotation

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Генерация пары токенов
function generateTokenPair(userId) {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);

const refreshToken = jwt.sign(
{ userId, type: 'refresh', jti: crypto.randomUUID() },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);

return { accessToken, refreshToken };
}

// Обновление токенов
app.post('/api/auth/refresh', async (req, res) => {
try {
const { refresh_token } = req.body;

if (!refresh_token) {
return res.status(401).json({ error: 'Refresh token required' });
}

// Проверяем refresh token
const decoded = jwt.verify(refresh_token, process.env.JWT_REFRESH_SECRET);

// Проверяем, что токен не в черном списке
const blacklisted = await db.query(
'SELECT id FROM blacklisted_tokens WHERE jti = $1',
[decoded.jti]
);

if (blacklisted.rows.length > 0) {
return res.status(401).json({ error: 'Token revoked' });
}

// Добавляем старый токен в черный список
await db.query(
'INSERT INTO blacklisted_tokens (jti, expires_at) VALUES ($1, $2)',
[decoded.jti, new Date(decoded.exp * 1000)]
);

// Генерируем новую пару токенов
const tokens = generateTokenPair(decoded.userId);

res.json(tokens);

} catch (error) {
console.error('Token refresh error:', error);
res.status(401).json({ error: 'Invalid refresh token' });
}
});

Защита от атак

Rate Limiting

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL);

// Глобальный rate limiting
const globalLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
windowMs: 15 * 60 * 1000, // 15 минут
max: 1000, // 1000 запросов на IP
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false
});

// Строгий rate limiting для аутентификации
const authLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
windowMs: 15 * 60 * 1000,
max: 5, // 5 попыток входа за 15 минут
skipSuccessfulRequests: true,
message: 'Too many login attempts',
onLimitReached: (req, res, options) => {
console.log(`Auth rate limit exceeded for IP: ${req.ip}`);
// Уведомление администратора при подозрительной активности
if (req.attempts > 10) {
notifyAdminSuspiciousActivity(req.ip, 'auth_brute_force');
}
}
});

// Применение rate limiting
app.use(globalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);

CSRF защита

const csrf = require('csurf');

// CSRF middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});

// Применение CSRF защиты к формам
app.use('/api', csrfProtection);

// Endpoint для получения CSRF токена
app.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});

// Frontend: отправка CSRF токена
async function apiRequest(url, options = {}) {
// Получаем CSRF токен
const csrfResponse = await fetch('/api/csrf-token');
const { csrfToken } = await csrfResponse.json();

return fetch(url, {
...options,
headers: {
...options.headers,
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
credentials: 'include'
});
}

SQL Injection защита

const { Pool } = require('pg');

// Настройка пула соединений с безопасными параметрами
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});

// Безопасная функция для выполнения запросов
async function safeQuery(text, params = []) {
const client = await pool.connect();
try {
// Логирование запросов для аудита
console.log('SQL Query:', { text, params: params.map(p => typeof p === 'string' && p.length > 100 ? '[LONG_STRING]' : p) });

const result = await client.query(text, params);
return result;
} catch (error) {
console.error('Database query error:', error);
throw error;
} finally {
client.release();
}
}

// Пример безопасного использования
app.get('/api/products/search', async (req, res) => {
try {
const { q, category, min_price, max_price } = req.query;

// Валидация входных данных
if (q && typeof q !== 'string') {
return res.status(400).json({ error: 'Invalid search query' });
}

if (min_price && isNaN(parseFloat(min_price))) {
return res.status(400).json({ error: 'Invalid min_price' });
}

// Параметризованный запрос
let query = `
SELECT id, name, description, price, image_url
FROM products
WHERE status = 'active'
`;
const params = [];
let paramCount = 0;

if (q) {
paramCount++;
query += ` AND (name ILIKE $${paramCount} OR description ILIKE $${paramCount})`;
params.push(`%${q}%`);
}

if (category) {
paramCount++;
query += ` AND category_id = $${paramCount}`;
params.push(category);
}

if (min_price) {
paramCount++;
query += ` AND price >= $${paramCount}`;
params.push(parseFloat(min_price));
}

if (max_price) {
paramCount++;
query += ` AND price <= $${paramCount}`;
params.push(parseFloat(max_price));
}

query += ' ORDER BY created_at DESC LIMIT 50';

const result = await safeQuery(query, params);
res.json(result.rows);

} catch (error) {
console.error('Product search error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

Шифрование данных

Шифрование чувствительных данных

const crypto = require('crypto');

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // 32 байта ключ
const ALGORITHM = 'aes-256-gcm';

// Шифрование
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(ALGORITHM, ENCRYPTION_KEY);
cipher.setAAD(Buffer.from('marketplace-data'));

let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');

const authTag = cipher.getAuthTag();

return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}

// Расшифрование
function decrypt(encryptedData) {
const decipher = crypto.createDecipher(ALGORITHM, ENCRYPTION_KEY);
decipher.setAAD(Buffer.from('marketplace-data'));
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));

let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');

return decrypted;
}

// Хеширование паролей
const bcrypt = require('bcryptjs');

async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}

async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}

// Шифрование PII данных перед сохранением
app.post('/api/users/profile', authenticateToken, async (req, res) => {
try {
const { phone, address, passport_number } = req.body;

// Шифруем чувствительные данные
const encryptedPhone = phone ? encrypt(phone) : null;
const encryptedAddress = address ? encrypt(address) : null;
const encryptedPassport = passport_number ? encrypt(passport_number) : null;

await safeQuery(`
UPDATE user_profiles
SET
phone_encrypted = $1,
phone_iv = $2,
phone_auth_tag = $3,
address_encrypted = $4,
address_iv = $5,
address_auth_tag = $6,
passport_encrypted = $7,
passport_iv = $8,
passport_auth_tag = $9,
updated_at = NOW()
WHERE user_id = $10
`, [
encryptedPhone?.encrypted, encryptedPhone?.iv, encryptedPhone?.authTag,
encryptedAddress?.encrypted, encryptedAddress?.iv, encryptedAddress?.authTag,
encryptedPassport?.encrypted, encryptedPassport?.iv, encryptedPassport?.authTag,
req.user.id
]);

res.json({ message: 'Profile updated successfully' });

} catch (error) {
console.error('Profile update error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

Аудит и логирование

Система аудита

// Middleware для логирования действий пользователей
function auditLogger(action) {
return async (req, res, next) => {
const startTime = Date.now();

// Логируем запрос
const auditLog = {
user_id: req.user?.id,
action: action,
ip_address: req.ip,
user_agent: req.get('User-Agent'),
request_method: req.method,
request_url: req.originalUrl,
request_body: sanitizeForLogging(req.body),
timestamp: new Date(),
session_id: req.sessionID
};

// Перехватываем ответ
const originalSend = res.send;
res.send = function(data) {
auditLog.response_status = res.statusCode;
auditLog.processing_time = Date.now() - startTime;
auditLog.response_size = Buffer.byteLength(data);

// Сохраняем аудит лог
saveAuditLog(auditLog);

originalSend.call(this, data);
};

next();
};
}

// Очистка данных для логирования
function sanitizeForLogging(data) {
const sensitiveFields = ['password', 'credit_card', 'ssn', 'passport'];
const sanitized = JSON.parse(JSON.stringify(data));

function redactSensitiveData(obj) {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
redactSensitiveData(obj[key]);
} else if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
obj[key] = '[REDACTED]';
}
}
}

redactSensitiveData(sanitized);
return sanitized;
}

// Сохранение аудит логов
async function saveAuditLog(auditLog) {
try {
await safeQuery(`
INSERT INTO audit_logs (
user_id, action, ip_address, user_agent, request_method, request_url,
request_body, response_status, processing_time, response_size,
timestamp, session_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`, [
auditLog.user_id,
auditLog.action,
auditLog.ip_address,
auditLog.user_agent,
auditLog.request_method,
auditLog.request_url,
JSON.stringify(auditLog.request_body),
auditLog.response_status,
auditLog.processing_time,
auditLog.response_size,
auditLog.timestamp,
auditLog.session_id
]);
} catch (error) {
console.error('Audit log save error:', error);
}
}

// Применение аудита к критическим операциям
app.post('/api/admin/users/:id/suspend',
authenticateToken,
requireRole('admin'),
auditLogger('user_suspension'),
async (req, res) => {
// Логика приостановки пользователя
}
);

Мониторинг безопасности

Обнаружение аномалий

// Система обнаружения подозрительной активности
class SecurityMonitor {
constructor() {
this.suspiciousPatterns = {
rapid_requests: { threshold: 100, window: 60000 }, // 100 запросов за минуту
failed_logins: { threshold: 5, window: 300000 }, // 5 неудачных входов за 5 минут
unusual_locations: { enabled: true },
suspicious_user_agents: [
'sqlmap', 'nikto', 'nmap', 'masscan', 'zap'
]
};
}

async checkSuspiciousActivity(req) {
const checks = [
this.checkRapidRequests(req),
this.checkSuspiciousUserAgent(req),
this.checkUnusualLocation(req),
this.checkSQLInjectionAttempts(req)
];

const results = await Promise.all(checks);
const threats = results.filter(result => result.threat);

if (threats.length > 0) {
await this.handleSecurityThreat(req, threats);
}

return threats;
}

async checkRapidRequests(req) {
const key = `requests:${req.ip}`;
const count = await redis.incr(key);

if (count === 1) {
await redis.expire(key, 60); // TTL 60 секунд
}

if (count > this.suspiciousPatterns.rapid_requests.threshold) {
return {
threat: true,
type: 'rapid_requests',
severity: 'high',
details: `${count} requests in 60 seconds from ${req.ip}`
};
}

return { threat: false };
}

checkSuspiciousUserAgent(req) {
const userAgent = req.get('User-Agent')?.toLowerCase() || '';
const suspicious = this.suspiciousPatterns.suspicious_user_agents.some(
pattern => userAgent.includes(pattern)
);

if (suspicious) {
return {
threat: true,
type: 'suspicious_user_agent',
severity: 'medium',
details: `Suspicious user agent: ${userAgent}`
};
}

return { threat: false };
}

async checkUnusualLocation(req) {
if (!req.user) return { threat: false };

// Получаем геолокацию по IP
const location = await getLocationByIP(req.ip);

// Проверяем предыдущие локации пользователя
const recentLocations = await safeQuery(`
SELECT DISTINCT country, city
FROM user_sessions
WHERE user_id = $1 AND created_at > NOW() - INTERVAL '30 days'
`, [req.user.id]);

const knownLocation = recentLocations.rows.some(
loc => loc.country === location.country
);

if (!knownLocation && location.country) {
return {
threat: true,
type: 'unusual_location',
severity: 'medium',
details: `Login from new country: ${location.country}`
};
}

return { threat: false };
}

checkSQLInjectionAttempts(req) {
const sqlPatterns = [
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION)\b)/i,
/(--|;|\/\*|\*\/)/,
/(\b(OR|AND)\s+\d+\s*=\s*\d+)/i,
/'[^']*'[^']*'/
];

const testStrings = [
req.originalUrl,
JSON.stringify(req.query),
JSON.stringify(req.body)
];

for (const testString of testStrings) {
for (const pattern of sqlPatterns) {
if (pattern.test(testString)) {
return {
threat: true,
type: 'sql_injection_attempt',
severity: 'critical',
details: `SQL injection pattern detected in: ${testString.substring(0, 100)}`
};
}
}
}

return { threat: false };
}

async handleSecurityThreat(req, threats) {
const criticalThreats = threats.filter(t => t.severity === 'critical');
const highThreats = threats.filter(t => t.severity === 'high');

// Логируем угрозу
await safeQuery(`
INSERT INTO security_events (
ip_address, user_id, threat_type, severity, details,
user_agent, url, timestamp
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
`, [
req.ip,
req.user?.id,
threats.map(t => t.type).join(', '),
Math.max(...threats.map(t => t.severity === 'critical' ? 3 : t.severity === 'high' ? 2 : 1)),
JSON.stringify(threats),
req.get('User-Agent'),
req.originalUrl
]);

// Критические угрозы - блокируем IP
if (criticalThreats.length > 0) {
await this.blockIP(req.ip, 'critical_threat');
await this.notifySecurityTeam('critical', req, threats);
}

// Высокие угрозы - временная блокировка
else if (highThreats.length > 0) {
await this.temporaryBlock(req.ip, 3600); // 1 час
await this.notifySecurityTeam('high', req, threats);
}
}

async blockIP(ip, reason) {
await safeQuery(`
INSERT INTO blocked_ips (ip_address, reason, blocked_at, expires_at)
VALUES ($1, $2, NOW(), NULL)
ON CONFLICT (ip_address) DO UPDATE SET blocked_at = NOW(), reason = $2
`, [ip, reason]);
}

async temporaryBlock(ip, durationSeconds) {
await safeQuery(`
INSERT INTO blocked_ips (ip_address, reason, blocked_at, expires_at)
VALUES ($1, 'temporary_block', NOW(), NOW() + INTERVAL '${durationSeconds} seconds')
ON CONFLICT (ip_address) DO UPDATE SET
blocked_at = NOW(),
expires_at = NOW() + INTERVAL '${durationSeconds} seconds'
`, [ip]);
}

async notifySecurityTeam(severity, req, threats) {
const message = `
Security Alert: ${severity.toUpperCase()}

IP: ${req.ip}
User: ${req.user?.email || 'Anonymous'}
URL: ${req.originalUrl}
User Agent: ${req.get('User-Agent')}

Threats detected:
${threats.map(t => `- ${t.type}: ${t.details}`).join('\n')}

Time: ${new Date().toISOString()}
`;

// Отправка в Slack/Discord/Email
await sendSecurityAlert(message, severity);
}
}

// Использование security monitor
const securityMonitor = new SecurityMonitor();

app.use(async (req, res, next) => {
try {
await securityMonitor.checkSuspiciousActivity(req);
next();
} catch (error) {
console.error('Security monitoring error:', error);
next();
}
});

Настройка HTTPS и SSL

Автоматическое обновление сертификатов

#!/bin/bash
# auto-renew-ssl.sh

# Обновление всех сертификатов Let's Encrypt
certbot renew --quiet

# Проверка статуса обновления
if [ $? -eq 0 ]; then
echo "SSL certificates renewed successfully"

# Перезагрузка Nginx
nginx -s reload

# Уведомление о успешном обновлении
curl -X POST \
-H 'Content-type: application/json' \
--data '{"text":"SSL certificates renewed successfully on marketplace server"}' \
$SLACK_WEBHOOK_URL
else
echo "SSL certificate renewal failed"

# Уведомление об ошибке
curl -X POST \
-H 'Content-type: application/json' \
--data '{"text":"⚠️ SSL certificate renewal FAILED on marketplace server"}' \
$SLACK_WEBHOOK_URL
fi

# Очистка старых логов
find /var/log/letsencrypt -name "*.log" -mtime +30 -delete

Конфигурация безопасного HTTPS

# /etc/nginx/sites-available/marketplace-ssl
server {
listen 443 ssl http2;
server_name example.com *.example.com;

# SSL Configuration
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

# SSL optimizations
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https:; frame-src https://js.stripe.com;" always;

# Hide server version
server_tokens off;

# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://marketplace-backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /api/auth/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://marketplace-backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com *.example.com;
return 301 https://$server_name$request_uri;
}