| name | security-baseline-dev |
| description | Базовая безопасность в реализации — валидация входных данных (Zod), secrets management, безопасные ошибки, auth/authz patterns, XSS/injection prevention, dependency audit, secure headers. DO/DON'T примеры. Активируй при написании любого кода, работающего с пользовательским вводом, auth, секретами, или при вопросах «как сделать безопасно». |
Skill: Security Baseline (Dev)
Конкретные DO/DON'T паттерны безопасности для каждодневной разработки.
Разделы:
- Input Validation
- Secrets Management
- Безопасные ошибки
- Auth/AuthZ
- XSS Prevention
- Injection Prevention
- Secure Headers
- Dependency Security
- Logging Security
- Anti-patterns
1. Input Validation
✅ DO: валидация на границе (API / form) через Zod
import { z } from 'zod';
const createUserSchema = z.object({
body: z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().toLowerCase(),
age: z.number().int().min(13).max(150).optional(),
role: z.enum(['user', 'admin']).default('user'),
}),
});
const result = createUserSchema.safeParse({ body: req.body });
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten().fieldErrors });
}
const { name, email, age, role } = result.data.body;
❌ DON'T: доверять входным данным
app.post('/api/users', (req, res) => {
db.users.insert(req.body);
});
if (req.body.email && typeof req.body.email === 'string') {
}
app.post('/api/users', validate(createUserSchema), controller.create);
✅ DO: sanitize для HTML (если принимаете rich text)
import DOMPurify from 'isomorphic-dompurify';
function sanitizeHtml(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target'],
});
}
2. Secrets Management
✅ DO: env variables + validation
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
API_KEY: z.string().min(16),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ Missing/invalid env vars:', result.error.flatten().fieldErrors);
process.exit(1);
}
export const config = Object.freeze(result.data);
✅ DO: .gitignore для секретов
# Secrets — NEVER commit
.env
.env.local
.env.production
*.pem
*.key
credentials.json
service-account.json
❌ DON'T: секреты в коде
const API_KEY = 'sk-1234567890abcdef';
const dbUrl = 'mongodb://user:password@host:27017/db';
const API_KEY = config.API_KEY;
const dbUrl = config.DATABASE_URL;
logger.info({ apiKey: config.API_KEY });
throw new Error(`Auth failed for key: ${apiKey}`);
3. Безопасные ошибки
✅ DO: разделять operational и programmer errors
export function errorHandler(logger) {
return (err, req, res, _next) => {
if (err.isOperational) {
return res.status(err.statusCode).json({
error: err.message,
});
}
logger.error({
err,
requestId: req.id,
method: req.method,
url: req.originalUrl,
});
res.status(500).json({
error: 'Internal server error',
});
};
}
❌ DON'T: утекать внутренности
res.status(500).json({
error: 'duplicate key value violates unique constraint "users_email_key"'
});
res.status(500).json({
error: err.message,
stack: err.stack,
});
if (!user) return res.status(404).json({ error: 'User not found' });
if (!passwordMatch) return res.status(401).json({ error: 'Wrong password' });
return res.status(401).json({ error: 'Invalid credentials' });
4. Auth/AuthZ
✅ DO: JWT в httpOnly cookie (не localStorage)
res.cookie('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/',
});
localStorage.setItem('token', jwt);
✅ DO: authZ проверки ДО операций
async function deleteCoupon(req, res) {
const coupon = await couponService.getById(req.params.id);
if (coupon.ownerId !== req.user.id && req.user.role !== 'admin') {
throw new ForbiddenError('You can only delete your own coupons');
}
await couponService.remove(coupon.id);
res.status(204).end();
}
async function deleteCoupon(req, res) {
await couponService.remove(req.params.id);
}
✅ DO: password hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
5. XSS Prevention
✅ DO: React автоматически escapes JSX
return <p>{userInput}</p>;
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
import DOMPurify from 'dompurify';
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />;
✅ DO: CSP header
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
},
},
}));
6. Injection Prevention
✅ DO: параметризованные запросы
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
const user = await db.users.findOne({ email: String(email) });
const user = await db.users.findOne({ email: req.body.email });
import { execFile } from 'node:child_process';
execFile('convert', [inputPath, outputPath], callback);
exec(`convert ${userInput} output.png`);
7. Secure Headers
✅ DO: helmet.js (минимальная настройка)
import helmet from 'helmet';
app.use(helmet());
✅ DO: CORS — whitelist origins
import cors from 'cors';
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
credentials: true,
}));
app.use(cors({ origin: '*' }));
8. Dependency Security
✅ DO: регулярный audit
npm audit
npm audit fix
npm audit --production
npm install --save-exact
✅ DO: минимизировать зависимости
import _ from 'lodash';
const unique = _.uniq(arr);
const unique = [...new Set(arr)];
import moment from 'moment';
new Intl.DateTimeFormat('ru', { dateStyle: 'short' }).format(date);
✅ DO: lockfile в репозитории
# ✅ Lockfile ДОЛЖЕН быть в git (reproducible builds)
# НЕ добавляй в .gitignore:
# package-lock.json ← НУЖЕН в git
# bun.lockb ← НУЖЕН в git
9. Logging Security
✅ DO: sanitize логи от PII и секретов
function sanitizeForLog(obj) {
const SENSITIVE_KEYS = ['password', 'token', 'secret', 'apiKey', 'authorization',
'cookie', 'ssn', 'creditCard', 'cardNumber', 'cvv'];
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
if (SENSITIVE_KEYS.some((s) => key.toLowerCase().includes(s))) {
return [key, '[REDACTED]'];
}
if (typeof value === 'object' && value !== null) {
return [key, sanitizeForLog(value)];
}
return [key, value];
})
);
}
logger.info(sanitizeForLog({ email: , : , : }));
✅ DO: pino redact (автоматическая фильтрация)
import pino from 'pino';
const logger = pino({
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie', '*.password', '*.token', '*.secret'],
censor: '[REDACTED]',
},
});
10. Anti-patterns
| ❌ Anti-pattern | ✅ Решение |
|---|
req.body без валидации | Zod schema + validate middleware |
| Секреты в коде / git | env vars + .gitignore + validation |
| Stack trace в response | Разные ответы для operational/programmer errors |
| JWT в localStorage | httpOnly + secure + sameSite cookie |
SELECT * WHERE id = '${id}' | Параметризованные запросы |
exec(userInput) | execFile(cmd, [args]) |
cors({ origin: '*' }) в prod | Whitelist origins |
| PII в логах | Redact / sanitizeForLog |
lodash ради 1 функции | Нативный JS/ES2025 |
| Нет npm audit | CI pipeline + регулярный audit |
md5(password) | bcrypt/argon2 с salt |
| Разные ошибки для login | Единое «Invalid credentials» |
Краткий чеклист (каждый PR)
См. также
$security-review — полный security review чеклист (Reviewer gate)
$node-express-beast-practices — Express middleware pipeline, error handler
$observability-logging — structured logging с redaction
$es2025-beast-practices — безопасная работа с данными