FINDINGS.md 13 KB

Findings: Аудит проекта PhotoPlaces

Дата аудита: 2026-06-13 Дата исправлений: 2026-06-14


Статус исправлений

✅ P0 — Исправлено

  1. WebSocket CheckOrigin — больше не return true. При пустом allowedOrigins в production соединения отклоняются.
  2. Sentinel error для "not your place" — заменён на ErrNotYourPlace с errors.Is-поддержкой.
  3. Dev-секреты в docker-compose.yml — вынесены в переменные ${VAR:-default}.
  4. env.prod — очищен от замусоренного placeholder Yandex-ключа.
  5. Docker secrets — config.go поддерживает _FILE suffix.
  6. JWT секреты без дефолтов в production — config.go требует JWT_SECRET, JWT_REFRESH_SECRET, ALLOWED_ORIGINS в production; дефолты ("dev-secret", "localhost:3000") запрещены, приложение паникует при старте.
  7. Token Reuse Detection — уже реализован: GetRevoked в репозитории, проверка в RefreshSession, отзыв всех токенов пользователя при обнаружении, обработка в хендлере.
  8. Скрытие внутренних ошибок в production — все хендлеры используют единый writeError с параметром err для логирования; в production 500 ошибки возвращают "internal server error" вместо деталей БД/логики.
  9. WebSocket heartbeat + pub/sub — рефакторинг бэкенда и интеграция во фронтенд:
    • Backend: Hub с register/unregister/broadcast, Run() горутина; Client с send chan, readPump/writePump горутинами; ping/pong heartbeat (60s pong wait, 54s ping interval); typed events ({"type":"visitors","data":{...}}).
    • Frontend: хук useWebSocket с auto-reconnect (exponential backoff 1→30s); MapView заменяет polling 60s на WS для отображения точек других посетителей.
    • Breaking change: старый формат {"visitors":[...]} не поддерживается.
  10. Ценовой race condition в бронированияхSELECT ... FOR UPDATE на places внутри транзакции в BookingRepo.Create(); цена рассчитывается атомарно (подзапрос внутри INSERT); убран placeRepo из BookingHandler; удалены мёртвые методы CancelOverlapping и CalculateTotalPrice.

✅ P1 — Исправлено

  1. Теги и фичи в листинге местPlaceFilter.IncludeTagsFeatures + batch-загрузка (GetTagsBatch/GetFeaturesBatch), запрос ?include=tags,features. N+1 проблема решена.
  2. Cursor-based пагинацияPlaceFilter.CursorRating, PaginatedPlaces с json-тегами, сервис возвращает *PaginatedPlaces, репозиторий поддерживает курсор для created_at и rating сортировок. API: ?cursor=<base64>&limit=N. Ответ: {"data":[...], "next_cursor":"...", "has_more":true}.
  3. Интеграционные тесты — репозитории: TestMain с подключением по DATABASE_URL_TEST, фикстуры (user, place, tag), автономные тесты с cleanTables. Booking: цена, overlap, place not found, same slot different place. Place: status filter, cursor pagination, batch tags.

✅ P1 — Исправлено

  1. Rate limiter — Redis-based имплементация подключена в main.go, in-memory как fallback при недоступности Redis (реализован корректный fallback: при недоступности Redis автоматически используется in-memory limiter; в production failOpen=false — возвращается 503 только если оба лимитера недоступны).
  2. string(rune(line)) — код в log.go уже использует strconv.Itoa. Баг был исправлен до аудита.
  3. strPtr дублирование — удалён из handlers/helpers.go и services/auth.go. Создан единый pointer package.
  4. contains/searchString — код в repository/reviews.go использует errors.As напрямую. Проблема не актуальна.

✅ Тесты — Добавлены

  1. Backend unit-тесты:
    • services/auth_test.go — 6 тестов (регистрация, дубликат email, неверный пароль, бан, валидация токена, невалидный токен)
    • services/places_test.go — 3 теста (создание, не-владелец, неизвестный action модерации)
  2. Frontend тесты:
    • vitest + @testing-library/react
    • ErrorBoundary.test.tsx — 2 теста

✅ Frontend — Исправлено

  1. ErrorBoundary — создан компонент, подключён в layout.tsx
  2. catch {} — в admin/tags/page.tsx заменён на обработку с сообщением пользователю
  3. console.error — в MapView.tsx заменён на тихий catch (карта работает с текущими данными)

✅ Архитектура — Улучшено

  1. Dependency Inversion — в сервисы добавлены интерфейсы PlaceRepo, UserRepo, RefreshTokenRepo для тестируемости
  2. CSP — в Caddyfile добавлен Content-Security-Policy
  3. WebSocket — добавлен isProd флаг для безопасного fallback origins
  4. docker-compose.yml — все пароли через переменные окружения

⏳ P2 — Отложено

  1. MapLibre/OSM — только Yandex Maps. Задача на V2.
  2. Тесты frontend — база настроена (vitest), нужно расширять покрытие.
  3. CI/CD.github/dependabot.yml есть, но нет workflow. Нужен GitHub Actions.

Сводка изменений

Файл Изменение
backend/internal/services/places.go Sentinel errors + интерфейс PlaceRepo
backend/internal/services/auth.go Интерфейсы UserRepo/RefreshTokenRepo, убран strPtr
backend/internal/handlers/websocket.go isProd + CheckOrigin без fallback true
backend/internal/handlers/helpers.go Удалён strPtr
backend/internal/handlers/tags.go strPtr → pointer.Str
backend/internal/pointer/ Новый пакет (Str, Int, Float64)
backend/cmd/api/main.go isProd для WS Hub; rate limiter fallback (Redis → in-memory)
backend/internal/config/config.go JWT секреты обязательны в production, запрещены дефолты
backend/internal/handlers/errors.go Новый: единый writeError с логированием и скрытием деталей в prod
backend/internal/handlers/ws_client.go Новый: Client readPump/writePump, ping/pong heartbeat
backend/internal/handlers/websocket.go Переписан: Hub с register/unregister/broadcast, Run(), typed events
backend/internal/handlers/*.go Все хендлеры обновлены: writeError(msg, err) вместо err.Error()
backend/internal/models/place.go PlaceFilter.IncludeTagsFeatures, CursorRating; PaginatedPlaces json-теги
backend/internal/services/places.go List: batch-загрузка тегов/фич, encodeCursor/DecodeCursor, возвращает *PaginatedPlaces
backend/internal/repository/places.go GetTagsBatch, GetFeaturesBatch; курсор для rating sort
backend/internal/handlers/places.go List: парсинг ?include=tags,features и ?cursor=
backend/internal/repository/bookings.go Create: транзакция + SELECT ... FOR UPDATE + атомарный расчёт цены; удалены мёртвые методы
backend/internal/handlers/bookings.go Убран placeRepo; ErrPlaceNotFound в хендлере; цена в репозитории
backend/internal/repository/integration_test.go Новый: TestMain, тестовые фикстуры (insertTestUser, insertTestPlace, insertTestTag, cleanTables)
backend/internal/repository/booking_repo_test.go Новый: booking create (price, overlap, place not found, same slot different place)
backend/internal/repository/place_repo_test.go Новый: list with filters, cursor pagination, GetTagsBatch
docker-compose.yml Все пароли через ${VAR:-default}
deploy/env.prod Очищен Yandex-ключ
deploy/Caddyfile CSP + Permissions-Policy
frontend/src/components/ErrorBoundary.tsx Новый компонент
frontend/src/app/layout.tsx ErrorBoundary обёртка
frontend/src/app/admin/tags/page.tsx Обработка ошибок
frontend/src/components/MapView.tsx console.error → тихий catch; polling → WebSocket visitor dots
frontend/src/hooks/useWebSocket.ts Новый: хук с auto-reconnect, отправкой позиции, приёмом списка посетителей
frontend/vitest.config.ts Новый файл
frontend/src/test/ setup.ts, ErrorBoundary.test.tsx
obsidian_data/Photoplaces_data/ 7 новых Zettelkasten-заметок

Исправления от 2026-06-24 (аудит neyrogovnarik)

P0 — Критические исправления

# Проблема Решение
1 Тесты не компилируютсяmockPlaceRepo.SetTags принимал []string, интерфейс ожидает []Tag; mockRefreshTokenRepo без GetRevoked Исправлены сигнатуры, добавлены недостающие методы
2 writeValidationError утекал деталиerr.Error() всегда возвращался в production В production: details: "validation failed"
3 SameSite=LaxMode — refresh token уязвим к CSRF В production: SameSite=StrictMode
4 WebSocket без auth и rate limit — анонимные visitor_update без ограничений Добавлен per-connection rate limiter (10/s burst 1), глобальный лимит 1000 соединений; логин из JWT
5 Race condition refresh — конкурентные refresh сбрасывают все сессии DeleteIfExists с проверкой RowsAffected; ErrTokenReused без RevokeAll
6 In-memory rate limiter использует RemoteAddr — за Caddy все IP одинаковы Переключён на GetClientIP() (читает X-Forwarded-For)

P1 — Исправлено

# Проблема Решение
7 appEnv не используется в 6 хендлерах Параметр удалён из конструкторов
8 Расчёт цены брониint() урезал дробные часы math.Ceil()
9 Нет проверки min_hours в бронированиях GetPlaceMinHours, валидация в Create
10 Race condition cancel брони UpdateStatusIfPendingWHERE status='pending'
11 Refresh token cleanup не вызывался Background goroutine каждые 6 часов
12 Frontend Place тип не совпадает с backend coordinateslat/lng; ApiError{"error":"..."}

P2 — Исправлено

# Проблема Решение
13 Нет лимита тела запроса http.MaxBytesReader(w, r.Body, 1MB) во всех хендлерах
14 Cleanup горутина RateLimiter не останавливается Добавлен stopCh канал + Stop() метод
15 DeleteTag использует body + query param Упрощено, но сохранено для обратной совместимости

Obsidian-заметки (добавлены)

  • [[atomic-csrf-protection]]
  • [[atomic-refresh-token-race-condition]]
  • [[atomic-unused-appenv-parameter]]
  • [[atomic-booking-price-truncation]]
  • [[decision-validation-error-production]]
  • [[MOC-security-patterns]]