feat(unfurl): превью ссылок с защитой от SSRF и кэшем в БД (Фаза 7)
Сервер сам загружает заголовок, описание и картинку страницы по ссылке из сообщения и отдаёт клиенту готовую карточку. Безопасность (главное здесь): - только http/https и без userinfo; запрет петли, частных сетей, link-local (169.254.169.254), CGNAT, multicast и IPv4-mapped вариантов; - проверка идёт по адресу, к которому реально открывается TCP (`net.Dialer.Control`), поэтому подмена DNS между проверкой и соединением (DNS rebinding) ничего не даёт; - не больше 3 редиректов, каждый хоп проверяется заново; таймаут 5 с, тело ≤ 512 КБ, только `text/html`; прокси из окружения игнорируются, cookie и авторизация не отправляются; в логи попадают только хост и код причины; - картинка по ссылке не скачивается — проверяется лишь её URL: экономия CPU на 1 vCPU и минус класс атак через декодирование. Кэш: таблица `link_previews` (миграция 00022, ключ — sha256 нормализованного URL), TTL по статусу (ok — сутки, empty/blocked — час, error — 10 минут). Ручка `GET /api/v1/link-previews?url=…` отвечает статусом (ok/empty/blocked/error) и карточкой только при ok; 20 новых загрузок в минуту на пользователя, кэшированные ответы лимит не тратят. `UNFURL_ENABLED=false` выключает функцию целиком, `features.unfurl_enabled` виден в `/meta`. Retention убирает истёкшие записи кэша. Тесты: 21 в `internal/unfurl` (включая DNS rebinding через локальный DNS-сервер, редирект во внутреннюю сеть, таймаут, лимиты размера и типа), ручки, store и миграция.
This commit is contained in:
@@ -78,6 +78,10 @@ type Config struct {
|
||||
// (mailto:… или https://…). Пустой ключ — Web Push выключен.
|
||||
VAPIDPrivateKey string
|
||||
VAPIDSubject string
|
||||
// Unfurl ссылок (AGENT.md Фаза 7): сервер сам загружает превью страниц по
|
||||
// ссылкам из сообщений. Выключенный unfurl не ходит в интернет вовсе.
|
||||
UnfurlEnabled bool
|
||||
UnfurlTimeout time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
@@ -127,6 +131,9 @@ func Load() (Config, error) {
|
||||
|
||||
VAPIDPrivateKey: env("VAPID_PRIVATE_KEY", ""),
|
||||
VAPIDSubject: env("VAPID_SUBJECT", ""),
|
||||
|
||||
UnfurlEnabled: envBool("UNFURL_ENABLED", true),
|
||||
UnfurlTimeout: time.Duration(envInt("UNFURL_TIMEOUT_SECONDS", 5)) * time.Second,
|
||||
}
|
||||
if cfg.FilesDomain == "" {
|
||||
cfg.FilesDomain = "files." + cfg.Domain
|
||||
|
||||
Reference in New Issue
Block a user