Add "Security"
125
Security.md
Normal file
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)
|
||||||
Reference in New Issue
Block a user