Оформление
Вопросы к защите РК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.
3. Какие флаги у ваших авторизационных cookie и что даёт каждый? Почему document.cookie пустой, а refresh-cookie после F5 пропадает из Application?
Вид: покажи в коде. Бэк — где ставятся 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пустой, потому что авторизационные cookieHttpOnly— так и задумано; в 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. - «Алгоритм берём из заголовка токена» — алгоритм фиксирует сервер.
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» — простые запросы идут без него.
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 один.
9. Защищает ли CORS API от посторонних? Почему curl получает ответ без CORS-заголовков?
Вид: понимание.
Критерии
Засчитывается, если:
- CORS проверяет браузер, а не сервер;
curl, Postman и скрипт злоумышленника на его машине CORS не соблюдают; - CORS решает только, может ли страница другого origin в браузере пользователя читать ответы нашего API; от посторонних API защищают аутентификация и проверка владельца;
- middleware добавляет
Access-Control-Allow-*, только если в запросе естьOrigin;curlего не ставит, поэтому заголовков нет — это не баг. Проверять надо с-H 'Origin: ...'.
Типичные неверные ответы:
- «CORS не пустит
curlк нашему API» —curlCORS игнорирует. - «В
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дойдёт до обработчика.
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 — одно и то же» — разное место выполнения чужого кода и разная защита.
12. Почему недостаточно поставить cookie SameSite=Lax?
Вид: понимание.
Критерии
Засчитывается, если названы хотя бы три причины из четырёх:
- поддомены с пользовательским контентом:
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.
13. Зачем CSRF-защита, если cookie с HttpOnly?
Вид: понимание.
Критерии
Засчитывается, если:
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.
14. Покажи Double Submit Cookie: где создаётся токен, как он попадает в запрос и где сервер его проверяет.
Вид: покажи в коде. Бэк — генерация токена, 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из тела перед сохранением» — перечень запрещённых полей ненадёжен (в Goencoding/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.
Источники
- OWASP: Cross-Site Request Forgery Prevention Cheat Sheet — Double Submit Cookie, login CSRF, проверка
Originи Fetch Metadata, XSS обходит CSRF-защиту - OWASP: Authentication Cheat Sheet, Password Storage Cheat Sheet — одинаковый ответ на неудачный вход, bcrypt и argon2id, предел 72 байта
- OWASP: Session Management Cheat Sheet — идентификатор сессии, новый идентификатор после входа, сроки, завершение сессии на сервере
- OWASP: JSON Web Token Cheat Sheet — открытая нагрузка, отзыв, хранение токена на клиенте
- OWASP: Insecure Direct Object Reference Prevention Cheat Sheet, Mass Assignment Cheat Sheet, Input Validation Cheat Sheet
- OWASP: HTML5 Security Cheat Sheet —
localStorageи CORS - MDN: Set-Cookie, Document: cookie — флаги cookie,
HttpOnly-cookie не видны JS - MDN: Cross-Origin Resource Sharing (CORS), Site, Origin
- RFC 6265bis (draft-ietf-httpbis-rfc6265bis) — cookie,
SameSite,Path, префиксы - Fetch Standard: CORS protocol — простые запросы, preflight без credentials,
Origin - Go: golang.org/x/crypto/argon2 —
IDKeyпринимает соль от вызывающего кода - RFC 9110: HTTP Semantics, 403 и 404 —
404вместо403, чтобы скрыть существование ресурса