Введение: почему безопасность TypeScript приложений важна с первого дня
TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. Многие начинающие разработчики ошибочно полагают, что типизация автоматически решает все проблемы безопасности. На самом деле TypeScript компилируется в обычный JavaScript, и все уязвимости, присущие JS, остаются актуальными. В этой статье мы разберем топ-10 уязвимостей, с которыми сталкиваются новички, и покажем, как их избежать.
1. XSS (Межсайтовый скриптинг)
XSS — одна из самых распространенных атак. Злоумышленник внедряет вредоносный скрипт на страницу, который выполняется в браузере пользователя.
Пример уязвимого кода:
function renderUserInput(input: string): string {
return `<div>${input}</div>`;
}
// Злоумышленник передает: <script>alert('XSS')</script>
Как исправить:
Всегда экранируйте пользовательский ввод. Используйте библиотеки вроде DOMPurify или встроенные методы экранирования:
import DOMPurify from 'dompurify';
function safeRender(input: string): string {
const clean = DOMPurify.sanitize(input);
return `<div>${clean}</div>`;
}
2. SQL-инъекции
Если вы формируете SQL-запросы путем конкатенации строк с пользовательским вводом, вы открываете дверь для SQL-инъекций.
Уязвимый код:
function getUser(username: string): Promise<User> {
const query = `SELECT * FROM users WHERE username = '${username}'`;
return db.execute(query);
}
Безопасный вариант:
function getUserSafe(username: string): Promise<User> {
const query = 'SELECT * FROM users WHERE username = $1';
return db.execute(query, [username]); // параметризованный запрос
}
3. Небезопасное хранение паролей
Никогда не храните пароли в открытом виде. Используйте bcrypt или argon2 для хеширования.
import bcrypt from 'bcrypt';
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
4. Небезопасные прямые ссылки на объекты (IDOR)
Когда пользователь может получить доступ к чужим данным, просто изменив ID в URL.
Пример:
// Уязвимо: любой пользователь может получить любой заказ
app.get('/api/order/:id', (req, res) => {
const order = getOrderById(req.params.id);
res.json(order);
});
// Безопасно: проверяем принадлежность
app.get('/api/order/:id', (req, res) => {
const order = getOrderById(req.params.id);
if (order.userId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(order);
});
5. Использование устаревших зависимостей
TypeScript проекты часто используют npm-пакеты. Устаревшие версии могут содержать известные уязвимости.
- Регулярно запускайте
npm audit - Используйте
npm updateдля обновления пакетов - Настройте автоматические проверки в CI/CD
6. Недостаточная валидация входных данных
TypeScript типы не проверяются в рантайме. Всегда валидируйте данные на сервере.
interface UserInput {
email: string;
age: number;
}
// Недостаточно: типы TypeScript не защищают в рантайме
function processUser(input: UserInput) {
// ...
}
// Правильно: используем Zod или Joi для валидации
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120)
});
function processUserSafe(input: unknown) {
const validated = UserSchema.parse(input);
// validated имеет тип UserInput
}
7. Утечка информации через ошибки
Не показывайте пользователю детали внутренних ошибок — это может раскрыть структуру вашего приложения.
// Плохо: показывает стектрейс
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack });
});
// Хорошо: логируем, но пользователю отдаем общее сообщение
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
});
8. Небезопасная настройка CORS
Слишком широкая политика CORS позволяет любому сайту делать запросы к вашему API.
// Плохо: разрешаем все
app.use(cors({ origin: '*' }));
// Хорошо: указываем конкретные домены
app.use(cors({
origin: ['https://mysite.com', 'https://admin.mysite.com']
}));
9. Prototype Pollution (Загрязнение прототипа)
Атака, при которой злоумышленник изменяет свойства прототипа Object.
// Уязвимый код
function merge(target: any, source: any) {
for (const key in source) {
target[key] = source[key];
}
}
// Злоумышленник передает: { "__proto__": { "polluted": true } }
// Защита: используйте Object.create(null) для создания чистых объектов
function safeMerge(target: Record<string, any>, source: Record<string, any>) {
for (const key of Object.keys(source)) {
if (key !== '__proto__' && key !== 'constructor') {
target[key] = source[key];
}
}
}
10. Неправильная обработка асинхронных ошибок
Необработанные промисы могут привести к утечкам памяти и неожиданному поведению.
// Плохо: не обрабатываем ошибки
async function fetchData() {
const data = await riskyOperation();
return data;
}
// Хорошо: используем try-catch и глобальный обработчик
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhand