Сьогодні команда зводить усе зроблене за модуль в один сайт і доводить його форми до стану, у якому їх не соромно віддати на злом сусідній команді
У кожного з вас на сервері лежить купка окремих сторінок. Книга з теми 1. Дашборд із теми 3. Опис AI-медіа з теми 4. Бот із теми 7. Журнал автоматизації з теми 6.
Кожна з них працює. Але працює вона тільки тоді, коли ти сам знаєш її адресу й сам її відкриваєш. Така окрема готова штука зветься артефактом.
З продуктом усе інакше. У нього є один вхід і спільна навігація. Є головна сторінка, яка за десять секунд пояснює чужій людині, що це й для кого. І є документація, за якою чужа людина знайде потрібне без вас. Саме цю різницю ви сьогодні й закриваєте.
README.md так, щоб чужа людина знайшла потрібне
без вас;| Етап | Хв | Що робимо |
|---|---|---|
| Команди й ролі | 10 | Розподіляємо ролі, підписуємо правила аудиту |
| Від артефактів до продукту | 15 | Схема архітектури: 5 компонентів модуля в одному продукті |
| Практика: інтеграція | 30 | Публікуємо продукт на team-<N>, зводимо 5 компонентів під спільну навігацію |
| Теорія: CSRF | 15 | Підроблена відправка форми — з токеном і без; чому HTTPS сам не рятує |
| Гігієна форм | 25 | CSRF-токен, honeypot, серверна валідація, ліміт частоти — 6-пунктний чеклист |
| Документація | 15 | Пишемо README.md (архітектура і ролі) і SECURITY.md |
| Автоперевірка | 5 | up, nav, 🔒 csrf, form_validate, form_escape |
| Підсумок | 5 | Пари команд для крос-аудиту й межі дозволеного на урок 16 |
| Разом | 120 |
Слова CSRF, honeypot і «відповідальне розкриття» поки нічого не означають. Розберемо кожне на своєму місці.
п'ять адрес, п'ять стилів, п'ять авторів
одна адреса, одна навігація, одна документація
README.md каже, хто що зробив і де воно лежить;SECURITY.md каже, куди писати про знайдену дірку.91.219.61.4/s/team-<N>
під однією навігацією;README.md (архітектура + ролі)
і SECURITY.md (куди писати про вразливість);README.md, SECURITY.md і PHP-обробники форм.
AI-медіа команда показує з екрана — відео й аудіо сервер не приймає,
на сервері живуть лише описи й media.json.Спільна навігація, один стиль і головна сторінка, яка пояснює себе за десять секунд
Найпоширеніша помилка збірки одна: навігація є тільки на головній.
Гість заходить у /bot/ за прямим посиланням — і не бачить,
куди йти далі. Про решту чотирьох блоків він просто не дізнається.
Правило просте: з будь-якої сторінки продукту видно всі інші.
<header class="site">
<a class="brand" href="/">Команда 3 · Шкільний медіацентр</a>
<nav>
<a href="/book/">Книга</a>
<a href="/dash/">Дашборд</a>
<a href="/media/">AI-медіа</a>
<a href="/bot/">Бот</a>
<a href="/events/">Автоматизація</a>
</nav>
</header>
Зверніть увагу на шляхи від кореня: /book/, а не
../book/. Такий шлях зветься абсолютним. Відносний
ламається, щойно сторінку перекладуть на рівень глибше. І ламається тихо,
без жодного повідомлення про помилку.
Якщо кожен лишить свій CSS, вийде п'ять різних сайтів в одній рамці.
Домовтеся про один файл /team.css. У ньому кольори,
шрифт, шапка й вигляд форм.
Особисті стилі блоку кладуть окремим файлом, після спільного. І тільки на те, що справді унікальне саме для цього блоку.
| Що узгоджуємо | Домовленість команди | Чому саме так |
|---|---|---|
| Головна сторінка блоку | завжди index.html
або index.php | адреса без імені файла:
/bot/ замість /bot/chat-page-final2.html |
| Імена тек | малими латиницею, без пробілів і без транслітерації | сервер розрізняє регістр:
/Book/ і /book/ — різні адреси |
| Спільні стилі | /team.css, підключається першим |
одна правка кольору — і змінилися всі п'ять блоків |
| Обробники форм | усі в /api/,
по одному файлу на форму | перевіряти безпеку в одному місці значно легше, ніж у п'яти |
| Дані | ~/data/, поза ~/www |
усе, що в ~/www, віддається по HTTP усьому світу |
Уявіть, що на сторінку зайшов учитель фізики. Про ваш модуль він не знає нічого. За десять секунд він має зрозуміти три речі: що це, для кого і що тут можна відкрити просто зараз.
«Проєкт учнів 10-Б» не відповідає на жодне з них. А ось так — відповідає: «Медіацентр школи: розклад заходів, бот-довідник і дашборд відвідуваності. Для учнів і батьків».
Команда звела продукт: у кожного блоку своя тека, посилання
з головної працюють. Але однокласник надіслав другові пряме посилання
на /dash/, і той написав: «а де решта? тут просто графік».
Що саме забули зробити?
У команді з чотирьох найдорожча втрата — не помилка в коді, а троє, які сидять і чекають на четвертого
Заблокувати одне одного можна двома способами. Або двоє правлять той самий файл. Або одна робота не може початися, поки не готова інша.
Обидві біди лікуються заздалегідь, поки ви ще домовляєтеся. У процесі вони вже не лікуються — тільки перечікуються.
| Роль | За що відповідає | Що віддає команді |
|---|---|---|
| Тімлід / інтегратор | головна сторінка, спільна
навігація, team.css, злиття блоків |
каркас і шапку — у перші 20 хвилин, щоб решта мала куди вставлятися |
| Інженер безпеки форм | один спільний
api/form.php з шістьма фільтрами, CSRF-токен, honeypot |
готовий обробник, який решта просто підключає до своїх форм |
| Технічний письменник | README.md,
SECURITY.md, тексти головної |
документацію, яку пише паралельно, а не наприкінці |
| Тестувальник | чек-лист із 15 пунктів, прогін перевірок, ведення списку знахідок | список того, що ще червоне, — оновлюваний уголос кожні 20 хвилин |
Ролей чотири, а команда буває з трьох або з п'яти. Тоді ролі об'єднують або ділять. Головне інше: біля кожної ролі має стояти ім'я. Роль без імені означає, що її не робить ніхто.
team.css, п'ять порожніх тек. Решта в цей час
готує свої файли локально, нічого не заливаючи.team.css — кажеш уголос, тімлід міняє. Так не буває,
що двоє залили різні версії одного файла по FTP і другий стер першого.Через годину роботи виявилося, що шапка на трьох сторінках
із п'яти стара: у ній немає посилання на /events/.
Двоє учасників по черзі заливали team.css і головну
по FTP. Що пішло не так і як цього уникнути на наступному уроці?
Одна теча — публічна й видима всьому світу, друга — приватна. Плутанина між ними коштує модуля
data/,
upload/ і .env у червону рамку не потрапляють
ніколи.Домовленість про імена виглядає дріб'язком. Рівно доти, доки хтось
не назве файл Індекс копія (2).html.
Кирилиця й пробіли в іменах на сервері працюють. Але посилання
на такий файл перетворюється на
%D0%86%D0%BD%D0%B4%D0%B5%D0%BA%D1%81. Прочитати це
неможливо, і помилка в такому посиланні шукається годинами.
media-manifest.json;index.html;/book/ і /events/ вроздріб, а домовлено;Team.css і team.css —
два різні файли, і на вашому ноутбуці під Windows це не видно.<link href="Team.css">, на сервері відкриється
без стилів. У консолі при цьому буде тихий 404.
Тому перевіряйте не в себе, а на сервері.Тестувальник команди зайшов на http://91.219.61.4/s/team-3/data/
і побачив список файлів, а в ньому form.jsonl з іменами
й телефонами всіх, хто заповнював форму. Що сталося
і що робити насамперед?
README.md і SECURITY.md: два файли, які читають чужіОдин пояснює, як усе влаштовано. Другий каже, що робити людині, яка знайшла у вас дірку, — і саме він відрізняє серйозний проєкт від шкільного
README.md — не про ідею, а про пристрійНайчастіша помилка — написати README як твір. «Ми вирішили
зробити сайт, який допоможе учням». Такий текст не читає ніхто.
Цей файл відповідає на питання людини, яка вже на сайті. Її цікавить дві речі: де що лежить і до кого йти з питанням.
# Шкільний медіацентр — команда 3 Сайт про заходи школи: розклад, бот-довідник і дашборд відвідуваності. Для учнів 5–11 класів і батьків. Адреса: http://91.219.61.4/s/team-3/ ## З чого складається | Блок | Адреса | Що робить | Звідки | Власник | |-----------|------------|------------------------------------|--------|---------| | Книга | /book/ | історія школи, зроблена з AI | тема 1 | Оля | | Дашборд | /dash/ | 3 діаграми відвідуваності гуртків | тема 3 | Максим | | AI-медіа | /media/ | описи згенерованих ілюстрацій | тема 4 | Даша | | Бот | /bot/ | відповідає на питання про школу | тема 7 | Тарас | | Події | /events/ | журнал заявок із форми Google | тема 6 | Ніна | ## Як воно працює технічно - сторінки статичні, спільна шапка й стилі — `/team.css`; - усі форми шлють POST на `/api/form.php`; він перевіряє CSRF-токен, валідує поля, екранує вивід і дописує рядок у `~/data/form.jsonl`; - бот звертається до `/api/chat.php`, ключ моделі лежить у `~/.env` (права 600); - журнал подій наповнює `/api/hook.php` — приймає підписані події ззовні; - дані живуть у `~/data/`, поза `~/www`, і по HTTP не віддаються. ## Чого тут навмисно немає - відео й аудіо: правило модуля — медіа показуємо з екрана, на сервер не кладемо; - персональних даних у дашборді: агреговані числа, без прізвищ. ## Як запустити локально Відкрити index.html у браузері. Форми при цьому не працюють — вони потребують PHP; перевіряйте їх на сервері.
Зверніть увагу на колонки «Звідки» і «Власник». Саме їх найчастіше забувають. І саме вони перетворюють текст на робочий документ: через місяць одразу видно, кого питати про блок і на якому уроці він з'явився.
SECURITY.md — і чому це не формальністьУявіть: хтось випадково помітив, що ваша форма приймає чужі дані. Людина хоче попередити. Куди їй писати?
Якщо відповіді на сайті немає, буде одне з двох. Або вона промовчить, і дірка лишиться відкритою. Або напише публічно в чат класу — з деталями, які прочитають усі, зокрема й ті, кому не варто.
SECURITY.md закриває саме цей розрив. Це не файл «щоб було».
Великі проєкти тримають такий роками, а стандарт
RFC 9116 описує його
машиночитаний варіант — /.well-known/security.txt.
# Як повідомити про вразливість Дякуємо, що читаєте це. Якщо ви знайшли у нашому проєкті помилку, що стосується безпеки, — напишіть нам, будь ласка, перш ніж розповідати про неї публічно. ## Контакт - пошта: team3-security@91.219.61.4 - або особисто: вчителю інформатики, кабінет 214 ## Що написати 1. адресу сторінки або запиту; 2. що саме ви зробили (кроки, які можна повторити); 3. що отримали — код відповіді, шматок виводу, знімок екрана; 4. чим це небезпечно, на вашу думку. ## Що обіцяємо ми - відповімо протягом 3 навчальних днів; - скажемо, підтвердилася знахідка чи ні, і що виправили; - подякуємо в цьому файлі, якщо ви не проти. ## Про що просимо вас - не змінювати й не видаляти чужі дані; - не влаштовувати навантаження на сервер; - не публікувати деталі, доки ми не виправимо. ## Подяки - (сюди потрапляють ті, хто нам допоміг)
Уяви, що ти помітив зламаний замок на сусідських дверях. Розумний порядок дій простий: спершу сказати сусідам, а не викласти фото дверей у чат будинку.
З дірками в сайтах домовилися так само. Той, хто знайшов, повідомляє приватно й дає час на виправлення. Той, чий сайт, не вдає, ніби нічого не було: виправляє і публічно дякує. Ця домовленість зветься відповідальним розкриттям.
Зобов'язання тут в обох сторін. У цьому вся суть: без другої половини перша перестає працювати.
Учениця з паралельного класу помітила дірку у вашій формі:
введене зберігається без екранування. Вона виклала в загальний чат
школи скріншот із працюючим <script>: «дивіться,
у них дірка». Що вона мала зробити інакше, якби у вас був
SECURITY.md?
Головна нова тема уроку. Атака, у якій зловмисник не краде твій пароль і не бачить твоїх даних — він просто змушує твій браузер зробити те, чого ти не хотів
Уяви гардероб у театрі. Ти віддав куртку й отримав номерок. Далі гардеробник не питає твого прізвища: він дивиться на номерок і видає куртку тому, хто цей номерок приніс.
У вебі так само. Коли ти входиш на сайт, сервер віддає браузеру
маленький рядок на кшталт sid=8f3a…. Це і є номерок,
а зветься він cookie сеансу. Далі браузер сам, без твоєї участі,
додає цю cookie до кожного запиту на цей сайт.
Ключове слово — кожного. Браузер не питає, чи ти цього запиту
хотів. Він дивиться лише на адресу отримувача. Запит іде
на 91.219.61.4/s/team-3? Отже, cookie цього сайту додається.
А хто цей запит почав — твоя сторінка чи чужа — браузера довгий час
не цікавило взагалі.
| Міф | Як насправді |
|---|---|
| «Зловмисник украв мій пароль» | ні: він жодного разу не бачив ні пароля, ні cookie — cookie надіслав браузер, а не сторінка |
| «Він прочитав мою переписку» | зазвичай ні: відповідь твого сервера чужій сторінці не віддається (це блокує політика одного походження). CSRF — про дію, а не про читання |
| «У мене HTTPS, я захищений» | ні: HTTPS шифрує канал. Запит атаки теж піде по HTTPS — охайно зашифрованим |
| «Це проблема старих сайтів» | ні: це проблема будь-якої форми, що змінює дані і покладається лише на cookie |
Однокласник надіслав тобі посилання на смішну картинку. Ти перейшов, посміявся, закрив. Через хвилину на твоєму сайті з'явився новий запис від твого імені. Пароля ти нікому не казав і на сайт відтоді не заходив. Як таке можливо?
Захист будується не на «звідки прийшов запит», а на «чи знає відправник секрет, який я показував тільки на своїй сторінці»
Логіка захисту коротка. Сервер віддає свою сторінку з формою і кладе в неї приховане поле з випадковим рядком. Той самий рядок він запам'ятовує в себе, у сеансі.
Коли приходить POST, сервер порівнює два рядки: той, що в полі, і той, що в сеансі. Збіглися — форма справді з нашої сторінки. Цей випадковий рядок і зветься токеном.
Чому це працює проти CSRF? Чужа сторінка може надіслати запит. Але прочитати вміст твоєї сторінки вона не може: браузер цього не дозволяє. Правило зветься політикою одного походження, same-origin policy. Тому й підставити правильний токен чужа сторінка не в змозі — вона його ніколи не бачила.
<?php
declare(strict_types=1);
// cookie сеансу: тільки по HTTPS, недосяжна для JS, не їде на чужі сайти
session_set_cookie_params([
'secure' => true, // лише по HTTPS
'httponly' => true, // JS не прочитає document.cookie
'samesite' => 'Lax', // другий рівень захисту, див. нижче
]);
session_start();
// токен народжується один раз на сеанс
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32)); // 64 шістнадцяткові символи
}
?>
<form method="post" action="/api/form.php">
<input type="hidden" name="csrf"
value="<?= htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8') ?>">
<label for="name">Ім'я</label>
<input id="name" name="name" maxlength="80" required>
<label for="msg">Повідомлення</label>
<textarea id="msg" name="msg" maxlength="1000" required></textarea>
<button type="submit">Надіслати</button>
</form>
<?php
declare(strict_types=1);
session_start();
// hash_equals порівнює рядки за сталий час — щоб токен не можна було
// підібрати посимвольно, вимірюючи час відповіді сервера
$sent = (string)($_POST['csrf'] ?? '');
$real = (string)($_SESSION['csrf'] ?? '');
if ($real === '' || !hash_equals($real, $sent)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
exit('403 · запит не з нашої сторінки');
}
// далі — решта фільтрів: валідація, honeypot, ліміт, екранування
hash_equals в обробнику немає. Сервер приймає будь-який POST,
і форма захищена суто на вигляд. Перевіряють це не тим, що токен видно
у вихідному коді сторінки. Перевіряють тим, що запит без нього
повертає 403.SameSiteСервер може попросити браузер про послугу: «не надсилай цю cookie,
якщо запит почався на іншому сайті». Прохання записують у самій cookie,
в атрибуті SameSite. Значень у нього три.
| Значення | Коли cookie надсилається | Наслідок для CSRF |
|---|---|---|
None | завжди, зокрема в міжсайтових запитах
(обов'язково разом із Secure) |
атака проходить: cookie поїде |
Lax | у своїх запитах і при переході за звичайним посиланням; у міжсайтовому POST — ні | типову форму-атаку зупиняє |
Strict | тільки у своїх запитах | зупиняє й переходи за посиланням — незручно для входу |
Здається, що Lax робить токен зайвим. Не робить, і причин
три.
Перша: Lax не рятує від запиту з піддомена того
самого сайту — для браузера це той самий сайт. Друга: він не рятує, якщо
дія змінює дані через звичайне GET-посилання. Третя: браузери
поводяться по-різному. Chrome і Edge вважають cookie без атрибута
такою, що має Lax. Інші браузери — не обов'язково.
SameSite=Lax — другий рівень. Він працює в браузері
користувача, і ви ним не керуєте. Ставте обидва, а покладайтеся
на перший.Учень поставив cookie сеансу SameSite=Strict і прибрав
CSRF-токен: «навіщо два захисти, якщо перший уже блокує». Автоперевірка
на його сервері все одно показала червоний csrf.
Що саме зробив чекер?
Пройди атаку так, як її бачить жертва. Потім увімкни токен і повтори ту саму атаку — крок за кроком, на тих самих даних
Значення cookie, час і текст записів — ілюстративні,
вигадані для симулятора. Формати заголовків, коди відповідей
і поведінка SameSite — справжні.
SameSite=None. У стрічці з'явиться чужий запис. Потім
натисніть «Спочатку» й увімкніть токен. Запит той самий, cookie та сама,
а результат уже 403. Третім заходом лишіть токен вимкненим, а
SameSite поставте Lax. Атака теж не пройде,
але подивіться уважно, на якому кроці вона зупинилася.У симуляторі з SameSite=Lax і без токена атака
не спрацювала. Учень робить висновок: «токен не потрібен, досить
правильної cookie». У чому слабке місце цього висновку?
Кожен фільтр ви вже десь бачили. Сьогодні вони вперше стають одним обробником, через який ідуть усі форми продукту
| Фільтр | Від чого рятує | Від чого не рятує |
|---|---|---|
| Обмеження розміру | від запиту на 50 МБ, який з'їдає пам'ять і місце на диску | від маленького, але злого запиту |
| CSRF-токен | від запиту, надісланого чужим сайтом від імені користувача | від спаму й від XSS: якщо людина сама заповнила форму, токен у неї правильний |
| Honeypot | від простих ботів, які заповнюють усі поля підряд | від людини й від бота, написаного саме під вашу форму |
| Ліміт частоти | від 500 надсилань за хвилину: і спаму, і навантаження | від одного акуратного шкідливого надсилання |
| Серверна валідація | від порожніх, надто довгих і неправильних за типом полів — навіть коли запит іде повз сторінку | від правильного за формою, але шкідливого вмісту |
| Екранування виводу | від XSS: чужий
<script> показується текстом, а не виконується |
ні від чого на вході — воно працює на виході |
У саду ставлять блюдце з солодкою водою: комахи злітаються туди й не лізуть у їжу на столі. Приманка робить свою роботу тим, що її помічають не всі.
У формі роль приманки грає зайве поле. Людина його не бачить: воно сховане стилями. Бот бачить усі поля підряд і заповнює їх усі. Прийшов запит із заповненим цим полем — писала машина, і зберігати такий запит не треба. Таке поле-приманку звуть honeypot, тобто «горщик з медом».
<?php
declare(strict_types=1);
session_start();
header('Content-Type: text/plain; charset=utf-8');
// ── ① розмір і метод ─────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('405 · тільки POST');
}
if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 8192) {
http_response_code(413);
exit('413 · запит завеликий');
}
// ── ② CSRF-токен ─────────────────────────────────────────────
$real = (string)($_SESSION['csrf'] ?? '');
$sent = (string)($_POST['csrf'] ?? '');
if ($real === '' || !hash_equals($real, $sent)) {
http_response_code(403);
exit('403 · запит не з нашої сторінки');
}
// ── ③ honeypot: поле, приховане в CSS, людина його не бачить ─
if (trim((string)($_POST['website'] ?? '')) !== '') {
http_response_code(200); // боту кажемо «все гаразд»
exit('Дякуємо!'); // але нічого не зберігаємо
}
// ── ④ ліміт частоти: 10 надсилань за 60 секунд з однієї адреси ─
$ip = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
$slot = sys_get_temp_dir() . '/rl_' . hash('sha256', $ip) . '.txt';
$hits = array_values(array_filter(
array_map('intval', file_exists($slot) ? file($slot) : []),
static fn(int $t): bool => $t > time() - 60
));
if (count($hits) >= 10) {
http_response_code(429);
header('Retry-After: 60');
exit('429 · забагато надсилань, спробуйте за хвилину');
}
$hits[] = time();
file_put_contents($slot, implode("\n", $hits) . "\n", LOCK_EX);
// ── ⑤ серверна валідація: клієнтської недостатньо ────────────
$name = trim((string)($_POST['name'] ?? ''));
$msg = trim((string)($_POST['msg'] ?? ''));
$errors = [];
if (mb_strlen($name) < 2 || mb_strlen($name) > 80) { $errors[] = 'name'; }
if (mb_strlen($msg) < 5 || mb_strlen($msg) > 1000) { $errors[] = 'msg'; }
if ($errors !== []) {
http_response_code(422);
exit('422 · не пройшли поля: ' . implode(', ', $errors));
}
// ── зберігаємо СИРІ дані, поза www ───────────────────────────
$row = ['ts' => date('c'), 'name' => $name, 'msg' => $msg];
file_put_contents(
getenv('HOME') . '/data/form.jsonl',
json_encode($row, JSON_UNESCAPED_UNICODE) . "\n",
FILE_APPEND | LOCK_EX
);
// ── ⑥ екранування — на ВИХОДІ, при показі ────────────────────
echo 'Дякуємо, ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '!';
О'Коннор перетворюється
на О'Коннор прямо в журналі, і полагодити це вже
нічим. Правило таке: у сховищі лежить те, що ввела людина. Перетворюють
рівно в момент показу — і своїм способом для кожного місця показу:
HTML, атрибут, JSON.type="hidden"
Поле type="hidden" бот теж бачить у коді й не заповнює.
Робочий honeypot — звичайне текстове поле з autocomplete="off"
і tabindex="-1", сховане правилом у таблиці стилів.
І обов'язково aria-hidden="true": інакше екранний читач
продиктує незрячому користувачеві пастку для ботів як звичайне
поле.Форма перевіряє довжину повідомлення через maxlength
і JavaScript — усе працює, у браузері довше 1000 символів не введеш.
Тестувальник надсилає з термінала curl тіло на 40 000
символів, і воно спокійно лягає в журнал. Що не так із логікою
«валідація вже є»?
Не тому, що «так прийнято», а тому, що по дорозі від класу до сервера є щонайменше троє, хто бачить трафік
Коли сторінка відкрита по http://, усе введене їде мережею
відкритим текстом. Не «якось зашифроване за замовчуванням». Саме відкритим
текстом, який читається як звичайний рядок.
Побачити його може будь-хто на шляху. Сусід у тій самій Wi-Fi-мережі. Адміністратор шкільного роутера. Провайдер.
Secure: із ним браузер
не надішле cookie по HTTP взагалі;action форми. Форма виглядатиме вашою, а дані підуть
кудись інде.action форми починається
з http://. Користувач бачить ознаку захисту, а дані їдуть
відкрито. Сучасні браузери таке блокують або попереджають. Але покладатися
на це не варто: перевіряйте action самі./api/form.php), а не з протоколом. Тоді сторінка
по HTTPS сама тягне все по HTTPS;Secure,
HttpOnly і SameSite=Lax;<img>, <script>
чи <link> з http:// —
це «змішаний вміст», і браузер його заблокує мовчки.Команда каже: «У нас же HTTPS і сертифікат у порядку. Навіщо ще CSRF-токен і перевірка полів на сервері?» Чим саме HTTPS тут не допомагає?
Тестувальник команди проганяє цей список уголос. Кожен пункт має команду, якою його перевіряють, — не «здається, працює», а код відповіді на екрані
Позначайте пункт тоді, коли команда виконана й вивід збігся з очікуваним. Галочки зберігаються в браузері й переживають перезавантаження сторінки.
Окремо стоять пункти з позначкою блокує. Вони вирішують долю здачі. Скільки б відсотків не набралося, поки такий пункт червоний, продукт не приймуть.
form_validate, form_escape і csrf
автоперевірка дивиться вже сьогодні. Ще два — perms
і no_media — наскрізні для всього модуля. Їх перевірять
на уроці 16, разом із чужим аудитом. У чек-листі вони стоять уже сьогодні,
щоб не рятувати їх за десять хвилин до захисту.91.219.61.4/s/team-<N>. Підставте свою
адресу. Токен у змінній T береться з живої сторінки форми.
Тому команди з ним запрацюють лише тоді, коли форма справді віддає
поле csrf.Команда закрила 13 пунктів із 15 — 87 %. Червоними лишилися
form_escape і пункт про доступність форми.
Тімлід каже: «87 — це десь одинадцять балів, здаємо».
Чому він помиляється?
Кроки 1—4 робить уся команда разом, далі — паралельно за ролями. Галочки зберігаються
isset замість
hash_equals перетворює захист на декорацію. Саме це
найчастіше знаходять на крос-аудиті.Чекер стукає у ваш продукт ззовні: перевіряє редирект на HTTPS, обходить сторінку й шле форми напряму. Спроби не обмежені
Демонстраційний режим: результат згенеровано для показу.
На сервері ця кнопка викликає POST /api/check, який запускає
/opt/lessons/15/check.sh від імені команди.
csrf і чи різне його значення у двох різних сеансах;README.md — і просить кожного учасника показати
свій рядок у таблиці ролей та відкрити свій блок;SECURITY.md — і питає, що команда зробить,
якщо про дірку напишуть за годину до захисту;Це те, що бачить учитель, коли виставляє оцінку.
| Складник | Вага | Результат |
|---|
hash_equals в обробнику — це не захист,
а порожнє поле в HTML;SECURITY.md — це не формальність, а єдиний спосіб
зробити так, щоб про вашу дірку сказали вам, а не в загальний чат.== замість hash_equals —
працює, але дає підбирати його по часу відповіді;<input type="hidden"> і
в GET-адресу «для зручності» — тепер він лежить у журналах сервера
й у чужій історії браузера;777 —
це блокуючий perms на уроці 16;README описує ідею, а не архітектуру: немає, який блок
хто зробив і де він лежить;SECURITY.md написано «пишіть нам» без жодного контакту;~/www/data/ — і весь журнал
з іменами й телефонами віддається по HTTP усьому світу;type="hidden" — бот його бачить
і не заповнює, фільтр не ловить нічого.Підготуйте 3-хвилинне демо продукту за схемою «проблема → рішення → показ». Прорепетируйте його з секундоміром. Розподіліть репліки заздалегідь: хто говорить про проблему, хто веде показ, хто відповідає на питання про безпеку. Регламент на захисті жорсткий: 3 хвилини і 2 хвилини питань.
Другим пунктом кожен учасник записує одне речення про свій блок. Воно знадобиться, якщо на захисті спитають саме про нього. У реченні — що цей блок робить і який фільтр форми в ньому найважливіший.
Усе, що на цій сторінці можна перевірити самому, має посилання. Те, що вигадано для прикладу, підписано просто в тексті
SameSite — значення
Strict, Lax, None,
вимога Secure для None і різниця
в поведінці браузерів за замовчуванням.
developer.mozilla.org · Set-Cookie · SameSiteSameSite і рекомендацію
трактувати відсутній атрибут як Lax.
datatracker.ietf.org · rfc6265bis403 Forbidden,
413 Content Too Large,
422 Unprocessable Content.
rfc-editor.org/rfc/rfc9110429 Too Many Requests
і заголовок Retry-After для ліміту частоти.
rfc-editor.org/rfc/rfc6585/.well-known/security.txt,
машиночитаного родича SECURITY.md.
rfc-editor.org/rfc/rfc9116SECURITY.md і те, чому файл кладуть
у корінь проєкту.
docs.github.com · SECURITY.mdrandom_bytes() і bin2hex() —
криптографічно стійке джерело випадковості; 32 байти дають рядок
із 64 шістнадцяткових символів.
php.net · random_byteshash_equals() — порівняння за сталий час
і пояснення, від чого воно захищає.
php.net · hash_equalssession_set_cookie_params() — параметри
secure, httponly, samesite
і вимога викликати функцію до session_start().
php.net · session_set_cookie_paramshtmlspecialchars() — прапорець
ENT_QUOTES і чому кодування треба вказувати явно.
php.net · htmlspecialcharshttp://.
developer.mozilla.org · Mixed contentStrict-Transport-Security,
яким сервер забороняє браузеру взагалі ходити до нього по HTTP.
rfc-editor.org/rfc/rfc6797curl, опція --write-out — змінні
%{http_code} і %{redirect_url}, з яких
складено команди чек-листа.
curl.se · manpage · --write-outfunny-cats.example
і 91.219.61.4. Домен верхнього рівня
.example зарезервовано саме для документації
(RFC 2606), тому він нічий і нікуди не веде.
rfc-editor.org/rfc/rfc2606&& у Windows PowerShell 5.1 —
чому в командах чек-листа для Windows треба писати
curl.exe, а не curl.
learn.microsoft.com · Invoke-WebRequestПовний список із поясненнями, що звідки взято і що є
ілюстративним прикладом, — у файлі urok-15-джерела.md
поруч із цією сторінкою.