CSRF: Атака через JSON API без кастомных заголовков
JSON API эндпоинт не требует кастомных заголовков. Используй CSRF для смены пароля admin через API.
mediumgolangPro
Задача
# CSRF через JSON API без кастомных заголовков
## Сценарий
Вы проводите аудит веб-приложения **SecureShop**. Приложение предоставляет JSON API для обновления профиля. В ходе ревью вы обнаружили эндпоинт `POST /api/profile` — он принимает JSON-тело с полями пользователя (включая `password`) и не требует никакой CSRF-защиты. Разработчик считает, что JSON API «по определению» безопасен от CSRF, потому что классические HTML-формы не отправляют JSON. Это широко распространённое заблуждение: браузерные API (`sendBeacon`, `fetch` в режиме `no-cors`, HTML-формы с `enctype=text/plain`) умеют обходить эту мнимую защиту.
## Цель
1. Убедитесь, что эндпоинт `POST /api/profile` действительно не проверяет CSRF-токен и не требует кастомных заголовков.
2. Сформируйте cross-origin payload, который отправит JSON-тело со сменой пароля для текущего залогиненного пользователя.
3. Доставьте payload в браузер с активной admin-сессией так, чтобы пароль администратора сменился на известное вам значение.
4. Войдите как `admin` с новым паролем и заберите флаг с `/admin`.
## Теория
**CSRF на JSON API** — расширение классической CSRF-атаки на REST-эндпоинты, принимающие JSON. Многие разработчики ошибочно полагают, что если приложение принимает только `Content-Type: application/json`, то атака невозможна — ведь `<form>` отправляет либо `application/x-www-form-urlencoded`, либо `multipart/form-data`. Это заблуждение: если сервер парсит тело запроса как JSON независимо от заголовка `Content-Type` (например, через `json.Unmarshal(body, ...)`), браузер можно убедить отправить JSON через несколько лазеек:
* `<form enctype="text/plain">` — особый формат отправки `name=value` пар, который при правильном подборе имён превращается в валидный JSON.
* `navigator.sendBeacon()` с `Blob` — позволяет указать произвольный `Content-Type` (включая `text/plain`), при этом браузер прикрепляет cookies жертвы.
* `fetch(..., {mode: 'no-cors'})` с `text/plain` — не вызывает CORS preflight, потому что считается «простым» запросом.
Реальная защита JSON API от CSRF строится на одном из двух механизмов: проверка обязательного **кастомного HTTP-заголовка** (браузер не может добавить его в «простом» cross-origin запросе без CORS preflight, а HTML-формы и `sendBeacon` не умеют добавлять кастомные заголовки вообще), либо классический **CSRF-токен** в заголовке или теле.
**Уязвимый паттерн:**
```go
func (h *Handler) APIUpdateProfile(w http.ResponseWriter, r *http.Request) {
user := h.getUser(r)
if user == nil { /* 401 */ return }
// Нет проверки источника, нет CSRF-токена, нет требования кастомного заголовка:
body, _ := io.ReadAll(r.Body)
var req ProfileUpdateRequest
json.Unmarshal(body, &req) // парсит JSON независимо от Content-Type
if req.Password != "" {
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
h.db.Exec("UPDATE users SET password=? WHERE id=?", hash, user.ID)
}
}
```
## Точка входа атаки
| Параметр | Значение |
|----------|----------|
| Ваш аккаунт | `demo` / `demo` |
| Уязвимый эндпоинт | `POST /api/profile` |
| Поле для смены | `password` (в JSON-теле) |
| Доставка боту | `POST /report` (бот залогинен как admin) |
| Цель | `GET /admin` (требует роль `admin`, показывает флаг) |
| Exploit Server / hosting | внешний хостинг для HTML-payload (см. панель лабы → вкладка HTML Payload) |