Перейти к содержимому

Вопросы к защите РК1 ​

Вопросы, по которым ментор проверяет, что команда понимает свою авторизацию, а не только написала её. Студенты читают их заранее и готовятся; критерии ответа спрятаны под «Критерии» — сначала попробуйте ответить сами.

Как пользоваться ​

  • Каждый студент отвечает про весь поток: фронтенд объясняет, что делает бэк, и наоборот. «Это делал не я» — не ответ на вопрос «понимание».
  • «Покажи в коде» — по своей роли. В пометке под вопросом сказано, что показывает бэк и что фронт. Показывает тот, кто писал; объяснить должен уметь любой.
  • Вопросы по вариантам задают команде с этим вариантом. Вопрос 24 (почему не хранить access в памяти) можно задать любой команде: в A и C он звучит как «почему вы не выбрали B».
  • Если фронт и API на одном origin, CORS у команды нет. На вопросы о CORS она отвечает, как было бы при отдельном API, и показывает, как устроен один origin (reverse proxy, прокси Vite).
  • Сколько вопросов задать и как перевести ответы в баллы, решает ментор. Проверки на стенде и готовые команды — в checklist.md.

Под каждым вопросом:

  • Вид — «понимание» (объяснить словами, можно со схемой) или «покажи в коде» (открыть свой код или DevTools и показать);
  • Засчитывается, если — что должно прозвучать в ответе;
  • Типичные неверные ответы — за что ответ не засчитывается, с коротким пояснением;
  • Разбор — где тема описана подробно.

Основы ​

1. Чем аутентификация отличается от авторизации и какой код ответа у каждой ошибки? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • аутентификация — «кто ты»: при входе сервер сверяет пароль с хешем и выдаёт сессию или токены, дальше узнаёт пользователя по cookie или токену на каждом запросе;
  • авторизация — «что тебе можно»: обработчик ручки проверяет, что файл или блок принадлежит тому, кто пришёл;
  • вход сам по себе ничего не разрешает: проверку владельца делает каждая ручка с ресурсом;
  • 401 — «не знаю, кто ты» (нет сессии, истёк access); 403 — «знаю, но нельзя»; на чужой ресурс в РК1 — 404, чтобы не раскрыть, что он существует; отказ CSRF-проверки — 403.

Типичные неверные ответы:

  • «Авторизация — это форма входа» — вход это аутентификация.
  • «Токен проверил middleware, значит, доступ проверен» — middleware знает, кто пришёл, но не знает, чей файл.
  • «На чужой файл отвечаем 403» — в РК1 ожидается 404; 403 в нашем API — отказ CSRF.
  • Путает 401 и 403.

Разбор: basics.md, access-control.md.

2. Чем site отличается от origin? Какой механизм браузера сравнивает site, а какой — origin? ​

Пары для разбора: https://example.ru и https://api.example.ru; https://api.example.ru и https://avatars.example.ru; http://localhost:5173 и http://localhost:8080.

Вид: понимание.

Критерии

Засчитывается, если:

  • origin — схема, хост и порт; site — схема и регистрируемый домен (example.ru), порт не учитывается;
  • example.ru и api.example.ru — разные origin, один site; api.example.ru и avatars.example.ru — тоже один site; два порта localhost — разные origin, один site;
  • SameSite у cookie сравнивает site, CORS и same-origin policy — origin;
  • вывод для проекта: запрос со страницы поддомена — same-site, и SameSite его не остановит.

Типичные неверные ответы:

  • «Поддомен — другой сайт, SameSite его отсечёт» — поддомен того же регистрируемого домена same-site.
  • «Origin — это домен» — без схемы и порта это не origin.
  • «localhost:5173 и localhost:8080 — один origin» — порты разные.

Разбор: basics.md.

Вид: покажи в коде. Бэк — где ставятся cookie и с какими флагами. Фронт — в DevTools: Application → Cookies, Network → запрос → Cookies, document.cookie в Console.

Критерии

Засчитывается, если:

  • HttpOnly — cookie нет в document.cookie, но браузер прикладывает её к запросам, в том числе к fetch из нашего кода;
  • Secure — только по HTTPS (localhost — исключение); SameSite=Lax или Strict — к запросам с другого site cookie не уходит (Lax пропускает только переходы верхнего уровня безопасным методом); Path — к каким путям прикладывать; Domain не ставим: он расширяет cookie на поддомены;
  • document.cookie пустой, потому что авторизационные cookie HttpOnly — так и задумано; в A и C при одном origin там видна только __Host-csrf, в B — ничего;
  • refresh-cookie с Path=/api/v1/auth пропала из Application не потому, что удалилась: панель показывает cookie для адресов, которые страница уже загрузила. После F5 она появится после первого запроса на /api/v1/auth/..., а в Network видна у каждого запроса, к которому приложена. Это особенность панели, а не защита: Path — не граница безопасности.

Типичные неверные ответы:

  • «HttpOnly — cookie не уходит с fetch» — уходит; HttpOnly запрещает только чтение из JS.
  • «Path прячет refresh-cookie, это защита» — не граница безопасности: скрипт с того же origin прочитает cookie с чужим Path, если у неё нет HttpOnly.
  • «Cookie пропала из Application — значит, удалилась, надо расширить Path до /» — она жива; с Path=/ долгий токен уходил бы с каждым запросом.
  • «Снимем HttpOnly, чтобы фронт видел токен» — нарушение минимума.
  • «Domain=example.ru сужает cookie до нашего домена» — расширяет на все поддомены.

Разбор: basics.md, pitfalls.md, pitfalls.md.

4. Где в браузере можно хранить токен, а где нельзя? Что меняет HttpOnly, если на странице XSS? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • localStorage, sessionStorage и cookie без HttpOnly — нарушение минимума: любой скрипт на нашем origin читает токен одной строкой и уносит;
  • HttpOnly-cookie — рекомендуемый способ: прочитать токен нельзя;
  • память JS — только для короткого access в варианте B, и XSS может его перехватить;
  • HttpOnly не спасает от XSS: скрипт на нашей странице шлёт запросы, браузер прикладывает cookie. Но действовать он может только через открытую вкладку жертвы и только пока она открыта; украденный токен работал бы и после;
  • цена HttpOnly-cookie — CSRF-защита: браузер приложит cookie и к запросу с чужого сайта.

Типичные неверные ответы:

  • «HttpOnly защищает от XSS» — ограничивает ущерб, но запросы от имени пользователя XSS делать может.
  • «localStorage безопасен: другой сайт его не прочитает» — его читает XSS на нашем origin.
  • «sessionStorage безопаснее» — скрипт читает его так же; разница только в сроке жизни.
  • «Токен в localStorage, чтобы пережить F5» — для этого есть refresh-cookie.

Разбор: basics.md, pitfalls.md.

5. Что лежит в JWT, кто может его прочитать и что сервер проверяет на каждом запросе? Зачем пара access и refresh? ​

Вид: понимание. Команда варианта C объясняет, почему у неё JWT нет и чем stateful-сессия отличается от stateless-токена.

Критерии

Засчитывается, если:

  • JWT — заголовок, нагрузка и подпись в base64url; нагрузку прочитает любой, у кого есть токен: base64url — кодирование, а не шифрование;
  • подпись гарантирует, что нагрузку не меняли и токен выпустил владелец ключа; в нагрузке — sub, exp и то, что нужно для проверки, без паролей и лишних данных;
  • сервер на каждом запросе проверяет подпись и exp; алгоритм зафиксирован на сервере, alg: none не принимается; ключ — в переменной окружения;
  • stateless-токен нельзя отозвать до exp, поэтому access короткий (в РК1 не дольше 15 минут); refresh долгий, сервер хранит его хеш и отзывает при logout сразу.

Типичные неверные ответы:

  • «JWT зашифрован, туда можно положить email и роль» — нагрузка открыта.
  • «Сервер ищет access в БД на каждом запросе» — в A и B access проверяется по подписи, без БД.
  • «После logout access сразу недействителен» — он работает до exp.
  • «Алгоритм берём из заголовка токена» — алгоритм фиксирует сервер.

Разбор: basics.md, basics.md.

6. Как в вашем проекте хранятся пароли и что сервер отвечает на неудачный вход? ​

Вид: покажи в коде. Бэк — хеширование при регистрации, сравнение при входе, ограничение длины, ответ на неудачный вход. Фронт — какое сообщение видит пользователь и что проверяет форма.

Критерии

Засчитывается, если:

  • bcrypt или argon2id — намеренно медленные функции;
  • соль случайная для каждого пароля: bcrypt генерирует её сам и хранит в строке хеша; для argon2id в Go соль из crypto/rand генерирует сервер и хранит рядом с хешем (обычно в строке формата $argon2id$v=19$m=…,t=…,p=…$соль$хеш);
  • длина пароля ограничена сверху на сервере; для bcrypt — не больше 72 байт, длинный пароль отклоняется с ошибкой, а не обрезается молча;
  • пароль не попадает в логи — ни при входе, ни при регистрации;
  • на «нет такого логина» и «неверный пароль» — одинаковые код и тело («Неверный логин или пароль»), и время ответа не выдаёт разницу;
  • проверки формы — для удобства, сервер проверяет всё сам. Плюс, если команда назовёт лимит попыток входа на аккаунт и на ручку.

Типичные неверные ответы:

  • «SHA-256 с солью» — быстрая функция, перебор на GPU дешёвый.
  • «Одна соль на всех» или «без соли» — одинаковые пароли дадут одинаковые хеши, и перебор сразу по всей базе станет дешевле.
  • «Пользователь не найден» и «Неверный пароль» — разные сообщения раскрывают, кто зарегистрирован.
  • «Длину ограничивает форма» — curl её обойдёт.
  • «Пароль шифруем, чтобы можно было восстановить» — пароли не шифруют, а хешируют.

Разбор: basics.md.

CORS ​

7. Что такое preflight, какие запросы вашего фронта его вызывают и почему он должен отвечать до авторизации? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • preflight — запрос OPTIONS, который браузер сам отправляет перед запросом на другой origin, если запрос не простой; без разрешения в ответе основной запрос не уходит вообще;
  • в проекте его вызывают Content-Type: application/json, свои заголовки X-CSRF-Token (A, C) и Authorization (B), методы PUT, PATCH, DELETE;
  • preflight идёт без cookie и без заголовков, которые ставит код; если OPTIONS попадает в auth-middleware, тот отвечает 401, а успешный preflight — только 200–299: основной запрос не уходит, хотя пользователь вошёл;
  • поэтому CORS-middleware стоит снаружи auth и отвечает на preflight сам;
  • при одном origin preflight нет вовсе.

Типичные неверные ответы:

  • «Preflight отправляет наш фронт» — его отправляет браузер, код его не видит и не задаёт.
  • «На OPTIONS тоже проверяем токен» — в preflight нет ни cookie, ни Authorization.
  • «Preflight перед каждым запросом, даже перед простым GET» — простые запросы идут без него.

Разбор: cors.md, cors.md.

8. Покажи настройку CORS: откуда белый список и что получит страница с https://evil.example? ​

Вид: покажи в коде. Бэк — настройка CORS-middleware и где она стоит относительно auth. Фронт — credentials: 'include' в обёртке над fetch. Если API на одном origin с фронтом — конфиг reverse proxy или прокси Vite и то, что CORS не включён «на всякий случай».

Критерии

Засчитывается, если:

  • белый список — точные строки вида https://example.ru, свой для каждого окружения, Origin сравнивается на равенство;
  • нет *, нет отражения любого Origin, нет null;
  • для запросов с cookie — конкретный origin в Access-Control-Allow-Origin и Access-Control-Allow-Credentials: true, иначе браузер ответ не отдаст;
  • для https://evil.example в ответе нет Access-Control-Allow-Origin — браузер не отправит запрос после preflight или не отдаст ответ JS;
  • фронт ставит credentials: 'include' на запросы к API на другом origin: без него cookie не уходят, а Set-Cookie из ответа браузер игнорирует. Плюс за Vary: Origin.

Типичные неверные ответы:

  • «Ставим *, чтобы точно работало» — с credentials браузер такой ответ не отдаст, а без них открывает чтение всем.
  • «Возвращаем Origin из запроса» — то же, что *, только ещё и с cookie.
  • «Проверяем, что Origin содержит example.ru» — пройдёт https://example.ru.evil.example.
  • «Allow-Origin: https://a.ru, https://b.ru» — список через запятую не работает, origin один.

Разбор: cors.md, cors.md.

9. Защищает ли CORS API от посторонних? Почему curl получает ответ без CORS-заголовков? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • CORS проверяет браузер, а не сервер; curl, Postman и скрипт злоумышленника на его машине CORS не соблюдают;
  • CORS решает только, может ли страница другого origin в браузере пользователя читать ответы нашего API; от посторонних API защищают аутентификация и проверка владельца;
  • middleware добавляет Access-Control-Allow-*, только если в запросе есть Origin; curl его не ставит, поэтому заголовков нет — это не баг. Проверять надо с -H 'Origin: ...'.

Типичные неверные ответы:

  • «CORS не пустит curl к нашему API» — curl CORS игнорирует.
  • «В curl нет CORS-заголовков, значит, CORS сломан» — нет Origin в запросе.

Разбор: cors.md, pitfalls.md.

10. Почему CORS не защищает от CSRF? Где preflight всё же помогает? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • CORS ограничивает чтение ответа, а не отправку запроса; простой запрос (HTML-форма POST, fetch с Content-Type: text/plain) уходит без preflight, сервер его выполняет;
  • для CSRF ответ не нужен — достаточно, что действие выполнено;
  • preflight помогает, когда в запросе Content-Type: application/json или X-CSRF-Token: чужая страница не отправит такой запрос без preflight, а preflight с чужого origin бэк не пропустит;
  • поэтому «только Content-Type: application/json на изменяющих ручках» работает, только если бэк отклоняет другие типы (415), а не разбирает любое тело как JSON;
  • защита от CSRF — SameSite, Double Submit и проверка Origin.

Типичные неверные ответы:

  • «У нас белый список CORS, значит, CSRF невозможен» — простой запрос уходит без проверки.
  • «Браузер не отправит запрос на чужой origin» — отправит, не отдаст только ответ.
  • «Бэк читает JSON из любого тела — и так защищено» — тогда форма с text/plain дойдёт до обработчика.

Разбор: cors.md, csrf.md.

CSRF ​

11. Что такое CSRF и в каком из вариантов A, B, C он актуален? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • браузер прикладывает cookie сам, кто бы ни начал запрос: чужая страница заставляет браузер жертвы отправить запрос к нашему API, и сервер принимает его за запрос пользователя; ответ злоумышленнику не нужен;
  • A и C — нужна защита на всех изменяющих ручках: авторизация в cookie;
  • B — только refresh-cookie: Authorization браузер сам не ставит, подделанный запрос к API придёт без токена и получит 401; плюс login CSRF на ручках входа;
  • отличие от XSS: при CSRF чужой код работает на чужой странице и не читает ни cookie, ни ответ; при XSS — на нашем origin и может всё, что наш фронт.

Типичные неверные ответы:

  • «CSRF — это кража cookie» — cookie злоумышленник не видит, браузер прикладывает их сам.
  • «В варианте B CSRF нет совсем» — остаются refresh-cookie и login CSRF.
  • «CSRF и XSS — одно и то же» — разное место выполнения чужого кода и разная защита.

Разбор: csrf.md, csrf.md, csrf.md.

Вид: понимание.

Критерии

Засчитывается, если названы хотя бы три причины из четырёх:

  • поддомены с пользовательским контентом: SameSite сравнивает site, скрипт на avatars.example.ru шлёт same-site запросы, и cookie уходят даже со Strict;
  • cookie tossing: поддомен может поставить cookie для example.ru и сломать наивный Double Submit (закрывает префикс __Host-);
  • GET с побочными эффектами: Lax пропускает cookie на переход по ссылке; поэтому GET не меняет данные;
  • старые браузеры и cookie без явного флага ведут себя по-разному.

Вывод: SameSite — первый слой; защита — Double Submit, __Host- и проверка Origin.

Типичные неверные ответы:

  • «Lax блокирует запросы со всех других доменов, включая поддомены» — поддомен того же site.
  • «SameSite заменяет CSRF-токен» — минимум требует и то и другое.

Разбор: csrf.md.

Вид: понимание.

Критерии

Засчитывается, если:

  • HttpOnly запрещает только чтение cookie из JS; к запросам браузер прикладывает её всё равно — и к запросу, который начала чужая страница. Это и есть CSRF;
  • HttpOnly закрывает другое — кражу токена через XSS. Платим за это обязательной CSRF-защитой: в A и C она в минимуме;
  • обратное тоже верно: CSRF-защита не спасает от XSS на нашем origin — скрипт прочитает __Host-csrf так же, как наш фронт.

Типичные неверные ответы:

  • «HttpOnly не даёт отправить cookie с чужого сайта» — отправку ограничивает SameSite, а не HttpOnly.
  • «Раз JS не видит cookie, чужой сайт тоже не может ею воспользоваться» — ему не нужно её видеть, браузер приложит её сам.

Разбор: basics.md, variant-a.md.

Вид: покажи в коде. Бэк — генерация токена, cookie, middleware: на каких методах и ручках стоит, как сравнивает, что отвечает. Фронт — откуда берётся токен до входа, когда читается, на какие запросы ставится заголовок.

Критерии

Засчитывается, если:

  • токен генерирует сервер криптостойким генератором (crypto/rand), не меньше 128 бит;
  • CSRF-cookie без HttpOnly (лучше с префиксом __Host-); фронт читает её и повторяет значение в заголовке X-CSRF-Token;
  • проверка на POST, PUT, PATCH, DELETE, включая login, register, refresh и logout; до входа cookie ставит middleware на любой ответ API без неё (например, на 401 стартового GET /api/v1/users/me), отдельного запроса за токеном нет; отказ — 403;
  • если API на отдельном origin, фронт не прочитает CSRF-cookie из document.cookie: сервер кладёт токен ещё и в заголовок ответа X-CSRF-Token (открыт через Access-Control-Expose-Headers), фронт держит последнее значение в памяти;
  • GET не меняет данные и токен не проверяет;
  • фронт читает токен перед каждым запросом: после входа сервер выдаёт новый;
  • почему чужая страница не подделает: значение cookie она прочитать не может, форма не ставит заголовки, а fetch со своим заголовком на чужой origin требует preflight. Плюс за сравнение за постоянное время.

Типичные неверные ответы:

  • «CSRF-cookie с HttpOnly» — фронт её не прочитает.
  • «Логин из проверки исключён» — минимум требует проверку на всех изменяющих методах; иначе login CSRF.
  • «Токен читаем один раз при старте» — после входа он устареет, и запросы получат 403.
  • «math/rand» — предсказуемые значения.

Разбор: csrf.md, variant-a.md, variant-a.md.

15. Почему отказ CSRF-проверки — 403, а не 401? Что будет, если ответить 401? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • на 401 фронт в A и B делает refresh и повторяет запрос; refresh не чинит CSRF-токен, повтор снова получает 401;
  • обёртка без ограничения повторов зацикливается; с одним повтором — лишний refresh и непонятная ошибка; если 401 получит сам refresh, фронт решит, что сессия кончилась, и выкинет пользователя на вход;
  • в C refresh нет, но на 401 фронт открывает страницу входа, хотя сессия жива;
  • 401 — «не знаю, кто ты», 403 — «знаю, но этот запрос не выполню»; на 403 фронт refresh не делает: показывает ошибку, а если 403 принёс новую CSRF-cookie — один раз повторяет запрос (csrf.md).

Типичные неверные ответы:

  • «Без разницы, лишь бы ошибка» — код определяет, что сделает фронт.
  • «401, потому что запрос не авторизован» — пользователь известен, неверен только токен.
  • «На 403 фронт делает refresh» — refresh нужен только на 401.

Разбор: csrf.md, pitfalls.md.

16. Что такое login CSRF и как он закрыт в вашем варианте? Зачем новый CSRF-токен после входа? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • чужая страница отправляет форму входа с логином и паролем злоумышленника; жертва работает в его аккаунте, и её файлы оказываются у него;
  • SameSite не помогает: злоумышленнику не нужны cookie жертвы, а cookie из ответа на отправку формы браузер сохраняет при любом SameSite;
  • A и C: Double Submit на login и register (cookie с токеном пришла с ответом на стартовый запрос к API); второй слой — проверка Origin / Sec-Fetch-Site (настойчиво рекомендуется);
  • B: CSRF-токена нет, основная защита — проверка Origin / Sec-Fetch-Site и только Content-Type: application/json на ручках входа;
  • токен до входа не привязан к пользователю, поэтому после входа сервер выдаёт новый. Плюс, если команда знает: при одном origin требуют Sec-Fetch-Site: same-origin (поддомен приходит как same-site), при отдельном API сравнивают Origin с белым списком.

Типичные неверные ответы:

  • «Логин защищать не надо: пароля жертвы у злоумышленника нет» — он и не нужен, входят в аккаунт злоумышленника.
  • «Закрыто SameSite» — cookie из ответа на навигацию сохраняется при любом SameSite.
  • «В варианте B login CSRF невозможен» — refresh-cookie злоумышленника сохранится и у жертвы.

Разбор: csrf.md, variant-b.md.

Доступ к данным ​

17. Боб открыл свой файл /files/41 и поменял в адресе 41 на 42 — это файл Алисы. Что должно произойти и где это проверяется? ​

Вид: покажи в коде. Бэк — обработчик GET /api/v1/files/{id}: откуда id пользователя и где условие на владельца. Фронт — что приложение показывает на этот ответ.

Критерии

Засчитывается, если:

  • это IDOR; закрывает его бэк: владелец — часть условия запроса к БД (WHERE id = $1 AND owner_id = $2) в каждой ручке с ресурсом;
  • id пользователя берётся из контекста, куда его положил auth-middleware по сессии или токену, и ниоткуда больше — ни из параметра, ни из тела, ни из заголовка;
  • ответ — 404, такой же, как на несуществующий файл: тот же код и то же тело;
  • фронт показывает «Файл не найден» и не делает refresh; скрытые ссылки и защищённые маршруты на фронте — удобство, а не проверка доступа.

Типичные неверные ответы:

  • «Фронт не покажет ссылку на чужой файл» — запрос можно отправить из curl или DevTools.
  • «У нас UUID, их не угадать» — id утекают через адреса, историю и логи; UUID не заменяет проверку владельца.
  • «Middleware проверяет, что пользователь вошёл» — это аутентификация, а не проверка владельца.
  • «Отвечаем 403» — в РК1 ожидается 404.

Разбор: access-control.md, access-control.md.

18. Почему на чужой файл сервер отвечает 404, а не 403? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • 403 сообщает «файл есть, но не ваш»: перебором id можно узнать, какие id заняты и сколько файлов в системе;
  • 404 одинаковый для «нет такого» и «чужой» — тот же код и то же тело; с условием на владельца в SQL это получается само;
  • так отвечают все ручки с ресурсом: одна ручка с 403 выдаст существование файла;
  • 401 — другое: нет сессии или истёк access; 403 в нашем API — отказ CSRF-проверки или проверки Origin.

Типичные неверные ответы:

  • «404 — только для несуществующих, для чужих правильно 403» — 403 встречается в других API, но в РК1 ожидается 404, и RFC 9110 это разрешает.
  • «Для чужого файла 404, но с текстом "нет доступа"» — тело тоже должно совпадать.

Разбор: access-control.md.

19. Как проверяется доступ при создании блока POST /api/v1/files/{id}/blocks? Откуда сервер берёт id файла? ​

Вид: покажи в коде. Бэк — обработчик создания блока и ручки изменения блока, если есть. Фронт — какой адрес и какое тело уходят при добавлении блока.

Критерии

Засчитывается, если:

  • блок принадлежит файлу, файл — пользователю; ручка проверяет всю цепочку;
  • id файла — только из URL; поля file_id в структуре тела нет, и сервер его не читает;
  • проверка родителя и вставка — одним запросом (INSERT ... SELECT ... WHERE f.id = $1 AND f.owner_id = $2) или в два шага; файла нет или он чужой — 404;
  • в ручках с id блока условие связывает блок с файлом, а файл — с владельцем, а не только файл из URL с пользователем;
  • фронт отправляет в теле только содержимое блока, без id файла и владельца.

Типичные неверные ответы:

  • «Проверяем, что пользователь вошёл» — Боб тоже вошёл.
  • «Берём file_id из тела» — Боб отправит запрос на свой файл с чужим file_id.
  • «Проверяем файл из URL, а блок обновляем по blockID» — Боб подставит id блока из файла Алисы.

Разбор: access-control.md.

20. Что вернёт GET /api/v1/files?user_id=1 от имени Боба? А POST /api/v1/files с "owner_id": 1 в теле? ​

Вид: покажи в коде. Бэк — запрос списка файлов и структура тела для создания файла. Фронт — какие параметры и поля уходят в этих запросах.

Критерии

Засчитывается, если:

  • список фильтруется по id пользователя из сессии или токена; параметр user_id сервер не принимает: игнорирует или отвечает 400; файлов Алисы в ответе нет;
  • тело разбирается в отдельную структуру (DTO), где есть только разрешённые поля; владельца сервер берёт из контекста, id и время создания назначает БД;
  • owner_id в теле даёт 400 (если лишние поля запрещены) или 201 с файлом у Боба — оба ответа правильные; файл у Алисы не появляется никогда;
  • фронту свой id для списка не нужен — сервер и так знает, кто пришёл. Плюс за лимит размера списка.

Типичные неверные ответы:

  • «Фронт передаёт свой user_id, сервер ему доверяет» — клиент подставит любой.
  • «Раскладываем тело прямо в модель БД» — это и есть mass assignment.
  • «Удаляем owner_id из тела перед сохранением» — перечень запрещённых полей ненадёжен (в Go encoding/json сопоставляет ключи без учёта регистра, OWNER_ID попадёт в то же поле); надёжно не иметь поля во входной структуре.

Разбор: access-control.md, access-control.md.

21. Почему проверки в форме недостаточно? Покажи, где сервер проверяет логин, пароль и тело запроса. ​

Вид: покажи в коде. Бэк — проверка полей регистрации и входа, разбор тела запроса. Фронт — правила формы и где они записаны так же, как на бэке.

Критерии

Засчитывается, если:

  • форму обходят curl, Postman или правка maxlength в DevTools, поэтому сервер проверяет всё заново; проверки в форме — для удобства пользователя;
  • проверка по белому списку: обязательность, тип, длина, формат, перечисление, диапазон; ошибка — 400 (или 422) с сообщением по полю, без текста ошибки БД и стека;
  • тело: лимит размера, строгий JSON (неизвестные поля, одно значение), на изменяющих ручках — Content-Type: application/json (если сделана рекомендуемая опция);
  • регулярки на сервере с якорями ^ и $; вход проверяется мягче регистрации, но пустые поля и слишком длинный пароль отклоняются 400 до похода в БД и bcrypt;
  • правила полей — общий контракт фронта и бэка, длина считается одинаково (байты или символы).

Типичные неверные ответы:

  • «У нас maxlength и pattern в форме» — обходится без фронта.
  • «Регулярка [a-z0-9_]{3,32} без якорей» — в Go совпадение ищется в любом месте строки, Alice! пройдёт.
  • «Длину пароля не ограничиваем» — bcrypt учитывает только 72 байта, а сервер не должен принимать пароль любого размера.
  • «Ошибку БД отдаём в ответе, так удобнее отлаживать» — внутренние детали в ответ не попадают.

Разбор: access-control.md, access-control.md.

Вариант A ​

22. Что происходит, когда истёк access? Покажи, как фронт делает один refresh на все 401. ​

Вид: покажи в коде. Фронт — обёртка над fetch: refresh, повтор, какие ответы не продлеваются. Бэк — ручка refresh: что проверяет и что выдаёт.

Критерии

Засчитывается, если:

  • запрос без access или с истёкшим получает 401; фронт делает один POST /api/v1/auth/refresh с X-CSRF-Token и повторяет запрос один раз;
  • одновременные 401 ждут один и тот же refresh, а не запускают свой каждый;
  • refresh-cookie уходит только на /api/v1/auth/... (узкий Path); сервер находит её хеш в БД, проверяет срок и ставит новые cookie access и refresh;
  • refresh не удался (401) — страница входа, без повторов; 401 на ручках /api/v1/auth/... обёртка не продлевает; 403 — показать ошибку, а не refresh;
  • плюс за ротацию refresh и обнаружение повторного использования (в требованиях РК1 её нет).

Типичные неверные ответы:

  • «Фронт читает exp из cookie access» — она HttpOnly, фронт её не видит.
  • «На каждый 401 — свой refresh» — лишняя нагрузка, а с ротацией — ложная тревога о краже.
  • «Refresh не удался — пробуем ещё раз» — бесконечный цикл.

Разбор: variant-a.md, variant-a.md.

23. Что остаётся действующим после logout в варианте A и почему access живёт не дольше 15 минут? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • logout удаляет на сервере сессию и все её refresh-записи и стирает access- и refresh-cookie (Max-Age=0, то же имя и Path, Secure); старый refresh после этого получает 401;
  • access до своего exp формально валиден: сервер проверяет его по подписи и о выходе не знает — это «отзыв access по истечении TTL»; поэтому access короткий;
  • только почистить cookie на клиенте — нарушение минимума: скопированный refresh продолжил бы работать;
  • ручка logout находит сессию по refresh-cookie и не требует живого access. Плюс, если команда знает, что мгновенный отзыв всего — это вариант C или список отозванных токенов, и что схема тогда перестаёт быть stateless.

Типичные неверные ответы:

  • «Logout удаляет cookie — этого достаточно» — сессия на сервере осталась бы.
  • «После logout access сразу не работает» — работает до exp.
  • «Logout — это сбросить состояние на фронте» — сервер о нём не узнает.

Разбор: variant-a.md, basics.md.

Вариант B ​

24. Почему access-токен в варианте B не рекомендуется хранить в памяти, если это не localStorage? ​

Вид: понимание. Можно задать любой команде: в A и C — «почему вы не выбрали B».

Критерии

Засчитывается, если:

  • память сама по себе — не утечка: другой сайт до переменной не дотянется (same-origin policy), браузер сам токен не отправляет, поэтому CSRF на API нет; после закрытия вкладки токена нигде нет;
  • проблема — XSS: внедрённый скрипт работает на нашем origin. Он перехватывает токен, подменив window.fetch, или сам вызывает refresh и читает новый access из ответа — поэтому «спрятать переменную получше» не помогает;
  • унесённым токеном злоумышленник пользуется со своей машины до exp, а пока вкладка открыта, может получать свежие; logout это окно не закрывает;
  • в варианте A та же XSS действует только через открытую вкладку: токены в HttpOnly-cookie вынести нельзя. A исключает вынос токена ценой обязательной CSRF-защиты;
  • B всё же допустим: refresh в HttpOnly-cookie не уносится, access живёт не больше 15 минут, refresh отзывается при logout сразу.

Типичные неверные ответы:

  • «В памяти токен никто не прочитает» — XSS перехватит его в запросах или получит новый через refresh.
  • «Спрячем в замыкание, и XSS не достанет» — переменная скрипту и не нужна.
  • «Плохо, потому что токен пропадает после F5» — это вопрос удобства, решается refresh-cookie, а не безопасности.
  • «Это то же самое, что localStorage» — localStorage переживает перезапуск браузера и читается одной строкой; память — только пока открыта вкладка. Но при XSS токен уносится в обоих случаях.

Разбор: variant-b.md, basics.md.

25. Как фронт в варианте B переживает F5 и новую вкладку? Покажи, где живёт access. ​

Вид: покажи в коде. Фронт — модуль API-клиента: где лежит access, что делается при старте, как ставится Authorization, что делает logout. Бэк — ответ на вход и refresh, откуда берётся access на защищённых ручках.

Критерии

Засчитывается, если:

  • access — в переменной модуля API-клиента или в замыкании; не в localStorage, sessionStorage и не в сторе с persist;
  • после F5 и в каждой новой вкладке памяти нет: при старте фронт вызывает POST /api/v1/auth/refresh (refresh-cookie осталась в браузере), получает access в теле и только потом запрашивает профиль;
  • бэк отдаёт access в теле ответа с Cache-Control: no-store, ставит refresh в HttpOnly-cookie с узким Path, а на защищённых ручках берёт access только из заголовка Authorization;
  • logout: сервер удаляет сессию и стирает cookie, фронт обнуляет переменную;
  • при отдельном API — credentials: 'include' на ручках /api/v1/auth/... и Authorization в Access-Control-Allow-Headers.

Типичные неверные ответы:

  • «Кладём access в localStorage, чтобы пережить F5» — нарушение минимума.
  • «Храним в Zustand с persist» — persist по умолчанию пишет в localStorage.
  • «Access в cookie без HttpOnly» — тоже нарушение минимума.
  • «Logout — обнулить переменную» — refresh-cookie осталась бы, и следующий старт молча вошёл бы снова.

Разбор: variant-b.md, variant-b.md, variant-b.md.

Вариант C ​

26. Почему в варианте C сессию можно завершить сразу и чем за это платим? ​

Вид: понимание.

Критерии

Засчитывается, если:

  • сервер ищет сессию в хранилище на каждом запросе; удалил запись — следующий запрос получает 401: logout, «выйти со всех устройств», блокировка срабатывают сразу, «хвоста» access нет;
  • цена: обращение к Redis или БД на каждый запрос; хранилище — точка отказа (недоступно — 503, а не 401); при нескольких экземплярах API нужно общее хранилище, сессии в памяти процесса не подходят; CSRF-защита обязательна, как в A;
  • у сессии два срока — неактивности и абсолютный, оба проверяет сервер; команда объясняет выбранные числа.

Типичные неверные ответы:

  • «Сессия кончается, когда браузер удаляет cookie» — запись на сервере живёт до своего срока, и копия session_id работает.
  • «Храним сессии в map в памяти» — теряются при перезапуске и не видны второму экземпляру.
  • «Redis не ответил — отвечаем 401» — выкинет на вход всех пользователей разом.

Разбор: variant-c.md, variant-c.md.

27. Покажи, как создаётся и проверяется сессия. ​

Вид: покажи в коде. Бэк — создание сессии при входе и поиск сессии в middleware. Фронт — обёртка над fetch: что делает на 401 и 403, откуда берёт CSRF-токен до входа.

Критерии

Засчитывается, если:

  • session_id — случайная строка из криптостойкого генератора, не меньше 128 бит; не id пользователя, не счётчик и не время входа;
  • при каждом входе сервер создаёт новую сессию и удаляет старую сессию браузера, а идентификатор от клиента не принимает (session fixation);
  • cookie HttpOnly, Secure, SameSite=Lax или Strict; команда объясняет выбор: __Host- с Path=/ или __Secure- с узким Path;
  • middleware ищет сессию, проверяет срок неактивности и абсолютный срок: нет или истекла — 401, хранилище недоступно — 503;
  • фронт: credentials: 'include', X-CSRF-Token на изменяющих запросах, CSRF-cookie до входа приносит ответ на стартовый GET /api/v1/users/me; на 401 — страница входа без refresh и повтора; на 403 — ошибка (кроме одного повтора входа, если cookie не было). Плюс за хеш session_id в хранилище вместо него самого.

Типичные неверные ответы:

  • «Срок сессии — это Max-Age у cookie» — срок проверяет сервер по записи в хранилище.
  • «После входа оставляем прежний session_id» — открывает session fixation.
  • «На 401 фронт делает refresh» — refresh в варианте C нет.

Разбор: variant-c.md, variant-c.md, variant-c.md.

Весь поток ​

28. Расскажи весь путь: пользователь регистрируется, входит, открывает страницу файла, добавляет блок, нажимает F5, через час возвращается к открытой вкладке и выходит. Что уходит в каждом запросе и что проверяет сервер? ​

Вид: понимание. Отвечает каждый студент, про фронт и бэк: фронтенд рассказывает, что проверяет сервер, бэкенд — что делает браузер и код фронта.

Критерии

Засчитывается, если по своему варианту названы шаги:

  • регистрация и вход — сервер валидирует поля, хеширует или сверяет пароль; в A и C CSRF-cookie уже пришла с ответом на стартовый запрос к API, на входе — X-CSRF-Token; в ответе — cookie с флагами (A, C) или access в теле и refresh-cookie (B); после входа — новый CSRF-токен (A, C);
  • страница файла — GET /api/v1/files/{id} с access-cookie, Authorization: Bearer или cookie сессии; сервер узнаёт пользователя и проверяет владельца, чужой файл — 404;
  • новый блок — POST с Content-Type: application/json и, в A и C, X-CSRF-Token; сервер проверяет токен, затем цепочку блок → файл → пользователь;
  • F5 — A и C: GET /api/v1/users/me с cookie (в A при 401 — refresh и повтор); B: сначала refresh, потом профиль;
  • через час — A и B: access истёк, 401 → refresh → повтор; C: сессия жива, если срок неактивности больше часа, иначе 401 и страница входа;
  • выход — сервер удаляет сессию или refresh-записи, авторизационные cookie стираются; в A и B access живёт до exp;
  • если API на отдельном origin — preflight перед запросами с JSON, X-CSRF-Token или Authorization и credentials: 'include'.

Типичные неверные ответы:

  • «Это делал бэкенд (фронтенд), я не знаю» — вопрос про весь поток.
  • «Флаг "вошёл" лежит в localStorage» — состояние «вошёл» фронт узнаёт у сервера.
  • Нет проверки владельца в рассказе про страницу файла и блок.
  • «После выхода всё отозвано мгновенно» — в A и B access работает до exp.

Разбор: потоки в variant-a.md, variant-b.md, variant-c.md; доступ — access-control.md.

Источники ​