Add "Security"

2026-04-12 20:46:43 +00:00
parent 88a89b97fb
commit 3fdd9cf12f

125
Security.md Normal file

@@ -0,0 +1,125 @@
# Пользователи и безопасность
## Управление пользователями
### Создание (admin only)
- Логин: уникальный, непустой
- Пароль: минимум 8 символов
- Роль: `admin` или `user`
- Проекты: массив доступных проектов (сейчас всегда `['vipavenue']`)
### Роли
| Возможность | admin | user |
|-------------|-------|------|
| Сборка писем | ✅ | ✅ |
| Рендер превью | ✅ | ✅ |
| FTP загрузка | ✅ | ✅ |
| Spell/Link check | ✅ | ✅ |
| Изменение settings | ✅ | ❌ |
| Изменение config (Yonote, URLs) | ✅ | ❌ |
| Управление пользователями | ✅ | ❌ |
### Пользовательские настройки
Сохраняются в профиле на сервере (PUT `/api/auth/preferences`):
- `theme` — 'light' / 'dark'
- `activePage` — последняя активная страница
- `previewZoom` — масштаб превью (40-100)
## Аудит безопасности (апрель 2026)
### 1. Pug Template Injection (CRITICAL → FIXED)
**Проблема:** Пользовательский текст `#{process.cwd()}` в поле блока попадал в `let.pug` и выполнялся на сервере при Pug-компиляции. Потенциально: `#{require('child_process').execSync('rm -rf /')}`.
**Фикс:** Экранирование `#{` и `!{` в пользовательском pug перед записью в файл:
```javascript
const pug = String(body.pug || '').replace(/([#!])\{/g, '$1\\{')
```
Не ломает миксины — `+spacerLine(40)` не использует интерполяцию.
Preheader дополнительно очищается от `\r\n\`\\` для предотвращения выхода из строки `+preheader("...")`.
### 2. XSS через iframe превью (HIGH → FIXED)
**Проблема:** `<script>alert('XSS')</script>` в HTML письма выполнялся в iframe без sandbox. Алерт выполнялся в контексте aspekter.ru — доступ к cookies, DOM основной страницы.
**Фикс:**
- `sandbox="allow-same-origin"` на iframe — скрипты заблокированы, DOM доступен
- Click detection переписан: вместо инъекции `<script>` в iframe, listener навешивается через `contentDocument.addEventListener()`
### 3. FTP Path Traversal (HIGH → FIXED)
**Проблема:** Параметр `folder` в FTP-эндпоинтах не проверялся. `folder: "../../etc"` позволял писать/читать/удалять файлы в любой директории FTP-сервера.
**Фикс:** Валидация во всех 3 эндпоинтах (upload, list, delete):
```javascript
if (/\.\./.test(folder) || folder.startsWith('/')) return send(400, { error: 'Недопустимый путь папки' })
```
### 4. Незащищённые настройки (HIGH → FIXED)
**Проблема:** Любой залогиненный юзер мог подменить:
- FTP-креды на свой сервер (перехват картинок)
- feedUrl (SSRF)
- Yonote-токен (DoS / exfiltration)
**Фикс:** Проверка роли admin:
```javascript
if (req.user?.role !== 'admin') return send(403, { error: 'Только admin может менять настройки' })
```
На `PUT settings` и `PUT config`.
### 5. SSRF (MEDIUM → FIXED)
**Проблема:** `isPublicUrl()` не блокировал:
- IPv6: `http://[::1]/`
- Hex IP: `http://0x7f000001/`
- Zero: `http://0.0.0.0/`
- DNS rebinding
**Фикс:**
```javascript
function isPublicUrl(url) {
if (!/^https?:\/\//i.test(url)) return false
if (/^https?:\/\/(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.|0x|\[|::)/i.test(url)) return false
try { const u = new URL(url); if (u.hostname === '0.0.0.0' || u.hostname.includes(':') || u.hostname.includes('[')) return false } catch { return false }
return true
}
```
### 6. Render DoS (MEDIUM → FIXED)
**Проблема:** Неограниченные параллельные рендеры → каждый спавнит Node-процесс с таймаутом 120 сек → OOM.
**Фикс:** `MAX_CONCURRENT_RENDERS = 3`, обёрнуто в `try/finally`:
```javascript
activeRenders++
try {
// ... render logic ...
} finally { activeRenders-- }
```
Первая реализация без try/finally приводила к застреванию счётчика.
### 7. Info Disclosure (LOW → FIXED)
**Проблема:** stderr от рендер-процесса возвращался юзеру (пути файлов, версии, стектрейсы).
**Фикс:** Статическое сообщение: `'Ошибка генерации. Проверьте PUG-шаблон.'`
### 8. Nginx — блокировка сканеров
```nginx
location ~* (\.env|\.git|\.php|\.sql|passwd|wp-admin|wp-login|phpmyadmin|cgi-bin) {
return 444;
}
```
444 — nginx закрывает соединение без ответа.
## Что НЕ закрыто (low priority)
- Typograf/speller прокси без авторизации (могут использовать сервер как прокси)
- `/uploads/` доступен без авторизации (by design — картинки для email)
- CSRF при отсутствии Origin+Referer (SameSite=Strict спасает)
- X-Forwarded-For spoofing (за nginx, low risk)
- DNS rebinding в SSRF (маловероятно — feedUrl меняет только admin)