Пошук уроків, статей та іншого контенту
Зрозумієте, як працюють cookies, атрибути Secure, HttpOnly та SameSite, і як вони захищають сесії.
Cookie — це невеликий фрагмент даних, який браузер зберігає для певного сайту та автоматично додає до наступних HTTP-запитів.
Cookie часто використовують для:
збереження налаштувань користувача;
зберігання ідентифікатора сесії;
аналітики;
авторизації;
тимчасового збереження стану.
Cookie не є базою даних і не повинна містити великі обсяги інформації. Для сесій зазвичай зберігають не самі дані користувача, а випадковий ідентифікатор:
session_id=8f4c...a912Сервер використовує цей ідентифікатор, щоб знайти сесію у своєму сховищі.
Типовий процес авторизації виглядає так:
Користувач надсилає логін і пароль на сервер.
Сервер перевіряє облікові дані.
Сервер створює випадковий ідентифікатор сесії.
Сервер зберігає сесію, наприклад:
session_id -> user_id, expiration, permissionsСервер надсилає браузеру cookie з ідентифікатором сесії.
Браузер автоматично додає cookie до наступних запитів.
Сервер знаходить сесію за ідентифікатором і розуміє, хто виконує запит.
Приклад відповіді сервера:
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=LaxНаступний запит браузера:
GET /profile HTTP/1.1
Cookie: session_id=abc123Саме cookie стає «перепусткою» до сесії, тому її викрадення може дозволити зловмиснику діяти від імені користувача.
SecureАтрибут Secure дозволяє надсилати cookie лише через HTTPS-з'єднання.
Set-Cookie: session_id=abc123; SecureБез Secure cookie може бути надіслана через звичайний HTTP. У незашифрованій мережі її потенційно можна перехопити.
Для production-середовища cookie сесії майже завжди повинна мати Secure:
Set-Cookie: session_id=abc123; Secure; HttpOnly; SameSite=LaxSecure захищає передавання cookie від перехоплення під час використання HTTPS, але не захищає від:
викрадення cookie через XSS, якщо cookie доступна JavaScript;
CSRF, якщо неправильно налаштований SameSite;
викрадення сесії через вразливість на сервері.
На локальній розробці сайти часто працюють через http://localhost. Браузери зазвичай мають спеціальне послаблення для localhost, але поведінку краще перевіряти у конкретному браузері. У production потрібен справжній HTTPS.
HttpOnlyАтрибут HttpOnly забороняє доступ до cookie з JavaScript через document.cookie.
Set-Cookie: session_id=abc123; HttpOnlyТака cookie:
надсилається браузером у HTTP-запитах;
доступна серверу;
не повертається через document.cookie.
Це особливо важливо для cookie сесії. Якщо на сторінці з'явиться XSS-вразливість, шкідливий скрипт не зможе напряму прочитати HttpOnly cookie.
console.log(document.cookie);Якщо session_id має атрибут HttpOnly, його не буде в результаті.
HttpOnly не усуває XSS повністю. Шкідливий скрипт усе ще може виконувати запити від імені користувача в межах поточної сторінки. Однак він не зможе просто скопіювати сесійну cookie і використати її в іншому місці.
SameSiteSameSite визначає, коли браузер може додавати cookie до запитів, що надходять з іншого сайту.
Set-Cookie: session_id=abc123; SameSite=LaxЄ три основні значення.
SameSite=StrictCookie надсилається лише в запитах з того самого сайту.
Set-Cookie: session_id=abc123; SameSite=StrictЦе найсуворіший режим. Він добре захищає від CSRF, але може погіршити деякі сценарії переходу на сайт із зовнішніх сторінок.
SameSite=LaxCookie надсилається в запитах з того самого сайту та в деяких безпечних переходах верхнього рівня, наприклад під час переходу за посиланням.
Set-Cookie: session_id=abc123; SameSite=LaxLax часто є хорошим значенням за замовчуванням для сесійних cookie.
SameSite=NoneCookie дозволено надсилати у міжсайтових запитах.
Set-Cookie: session_id=abc123; SameSite=None; SecureДля SameSite=None обов'язково потрібен Secure. Такий режим може знадобитися для:
вбудованих віджетів;
iframe;
окремого frontend і backend на різних сайтах;
деяких сценаріїв сторонньої автентифікації.
Водночас SameSite=None збільшує ризик CSRF, тому для небезпечних операцій потрібно додатково використовувати CSRF-токени.
CSRF — атака, під час якої зловмисник змушує браузер авторизованого користувача виконати небажану дію на іншому сайті.
Наприклад, користувач увійшов у bank.example. Потім він відкрив шкідливу сторінку, яка намагається відправити запит:
<form action="https://bank.example/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="recipient" value="attacker">
</form>
<script>
document.forms[0].submit();
</script>Якщо браузер додасть cookie сесії до цього запиту, сервер може помилково сприйняти його як запит користувача.
Захист складається з кількох рівнів:
використовувати SameSite=Lax або SameSite=Strict;
застосовувати CSRF-токени для операцій, що змінюють дані;
перевіряти заголовки Origin або Referer на сервері;
не використовувати GET для небезпечних операцій;
перевіряти автентифікацію та права доступу на сервері.
SameSite не замінює CSRF-токениПолітика SameSite залежить від браузера, типу запиту та архітектури застосунку. Вона також може бути несумісною з деякими легітимними міжсайтовими сценаріями.
Для особливо важливих операцій краще використовувати і SameSite, і CSRF-токен.
PathВизначає шлях, для якого cookie буде надсилатися:
Set-Cookie: theme=dark; Path=/Cookie з Path=/admin надсилатиметься для URL під /admin, але не для /profile.
Для cookie сесії зазвичай використовують:
Path=/DomainВизначає домен, для якого cookie доступна:
Set-Cookie: preference=compact; Domain=example.comCookie без Domain є host-only cookie: вона прив'язана саме до хоста, який її встановив. Це безпечніше за широке налаштування домену.
Не варто без потреби встановлювати cookie для всього домену, особливо якщо на піддоменах працюють різні застосунки.
Max-AgeВизначає час життя cookie у секундах:
Set-Cookie: session_id=abc123; Max-Age=3600У цьому прикладі cookie діятиме одну годину.
ExpiresВизначає точну дату завершення дії:
Set-Cookie: session_id=abc123; Expires=Wed, 29 Jul 2026 12:00:00 GMTЯкщо не вказати Max-Age або Expires, cookie зазвичай є сесійною і видаляється після завершення сесії браузера. Поведінка може залежати від налаштувань браузера.
Якщо задані обидва атрибути, Max-Age має пріоритет.
Щоб видалити cookie, сервер зазвичай надсилає її з нульовим часом життя:
Set-Cookie: session_id=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=LaxДля видалення повинні збігатися важливі атрибути, зокрема Path і Domain, якщо їх було задано під час створення.
Префікси допомагають браузеру перевіряти додаткові вимоги до cookie.
__Secure-Cookie з таким префіксом повинна мати Secure і встановлюватися через HTTPS:
Set-Cookie: __Secure-session_id=abc123; Secure; Path=/__Host-Cookie з префіксом __Host- повинна:
мати Secure;
встановлюватися через HTTPS;
мати Path=/;
не мати атрибута Domain.
Set-Cookie: __Host-session_id=abc123; Secure; Path=/; HttpOnly; SameSite=LaxДля сесійних cookie це часто один із найбезпечніших варіантів, якщо архітектура застосунку дозволяє його використати.
document.cookieJavaScript може читати та змінювати лише cookie без HttpOnly.
document.cookie = "theme=dark; Max-Age=3600; Path=/";
console.log(document.cookie);Важливо:
document.cookie повертає один рядок із доступними cookie;
JavaScript не може прочитати HttpOnly cookie;
JavaScript не може встановити HttpOnly;
атрибут Secure можна вказати під час встановлення cookie з HTTPS;
cookie автоматично надсилаються браузером, якщо відповідають умовам домену, шляху та SameSite.
Не слід зберігати токени авторизації в localStorage лише тому, що з ним зручніше працювати. Дані з localStorage доступні JavaScript, тому XSS-вразливість може призвести до їх викрадення.
Нижче наведено мінімальний приклад на Node.js без сторонніх бібліотек. Він:
створює сесію після входу;
встановлює HttpOnly cookie;
читає cookie на захищеному маршруті;
видаляє сесію під час виходу.
Це навчальний приклад. Зберігання сесій у Map не підходить для production, оскільки дані буде втрачено після перезапуску процесу, а кілька серверів не матимуть спільного сховища.
const http = require("node:http");
const crypto = require("node:crypto");
const sessions = new Map();
function parseCookies(request) {
const header = request.headers.cookie || "";
return Object.fromEntries(
header
.split(";")
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const separatorIndex = part.indexOf("=");
if (separatorIndex === -1) {
return [part, ""];
}
const name = part.slice(0, separatorIndex);
const value = part.slice(separatorIndex + 1);
return [name, decodeURIComponent(value)];
})
);
}
function sendJson(response, statusCode, data, headers = {}) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
...headers,
});
response.end(JSON.stringify(data));
}
const server = http.createServer((request, response) => {
const cookies = parseCookies(request);
const sessionId = cookies["__Host-session_id"];
if (request.method === "POST" && request.url === "/login") {
// У реальному застосунку пароль потрібно перевіряти через базу даних.
const newSessionId = crypto.randomBytes(32).toString("hex");
sessions.set(newSessionId, {
userId: 42,
createdAt: Date.now(),
});
sendJson(
response,
200,
{ message: "Вхід виконано" },
{
// Для локального HTTP Secure прибрано. У production використовуйте HTTPS і Secure.
"Set-Cookie": [
`__Host-session_id=${newSessionId}; HttpOnly; Path=/; SameSite=Lax; Max-Age=3600`,
],
}
);
return;
}
if (request.method === "GET" && request.url === "/me") {
const session = sessionId ? sessions.get(sessionId) : undefined;
if (!session) {
sendJson(response, 401, { error: "Неавторизований запит" });
return;
}
sendJson(response, 200, {
userId: session.userId,
message: "Це захищені дані",
});
return;
}
if (request.method === "POST" && request.url === "/logout") {
if (sessionId) {
sessions.delete(sessionId);
}
sendJson(
response,
200,
{ message: "Вихід виконано" },
{
"Set-Cookie": [
"__Host-session_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0",
],
}
);
return;
}
sendJson(response, 404, { error: "Маршрут не знайдено" });
});
server.listen(3000, () => {
console.log("Сервер працює на http://localhost:3000");
});Запуск:
node server.jsТестування за допомогою curl:
curl -i -c cookies.txt -X POST http://localhost:3000/login
curl -i -b cookies.txt http://localhost:3000/me
curl -i -b cookies.txt -c cookies.txt -X POST http://localhost:3000/logoutУ production cookie сесії повинна мати також Secure:
Set-Cookie: __Host-session_id=...; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=3600fetchДля запитів до того самого сайту браузер зазвичай надсилає відповідні cookie автоматично.
async function loadCurrentUser() {
const response = await fetch("/me");
if (response.status === 401) {
console.log("Користувач не авторизований");
return;
}
if (!response.ok) {
throw new Error("Не вдалося завантажити профіль");
}
const user = await response.json();
console.log(user);
}
loadCurrentUser().catch(console.error);Для cross-origin запитів потрібно явно вказати:
fetch("https://api.example.com/me", {
credentials: "include",
});У такому випадку сервер також повинен правильно налаштувати CORS:
дозволити конкретний Origin, а не *;
дозволити credentials;
надсилати відповідний Access-Control-Allow-Origin.
Не можна використовувати Access-Control-Allow-Origin: * разом із credentials.
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueПісля успішного входу сервер повинен створити новий ідентифікатор сесії, а не продовжувати використовувати ідентифікатор, який міг бути відомий до авторизації.
Це захищає від session fixation — атаки, коли зловмисник заздалегідь нав'язує жертві відомий ідентифікатор сесії.
Також бажано змінювати ідентифікатор:
після входу;
після зміни привілеїв;
після підвищення рівня доступу;
іноді після відновлення пароля.
Ідентифікатор сесії повинен бути:
випадковим;
достатньо довгим;
непередбачуваним;
без змістовної інформації про користувача;
перевіреним на сервері;
обмеженим за часом життя.
Не використовуйте як ідентифікатор:
user_id=42
email=user@example.com
timestamp=1720000000Такі значення легко вгадати або підробити.
Краще використовувати криптографічно стійкий генератор випадкових даних, як у прикладі з crypto.randomBytes.
Безпечна cookie — лише частина захисту. Сервер також повинен:
зберігати сесії у надійному сховищі;
мати термін завершення неактивної сесії;
перевіряти права доступу для кожної операції;
видаляти сесію після виходу;
інвалідувати всі сесії після критичної зміни пароля, якщо це потрібно політикою безпеки;
не зберігати паролі у відкритому вигляді;
використовувати захист від перебору паролів;
не передавати ідентифікатор сесії в URL.
Сесію не слід передавати так:
https://example.com/profile?session_id=abc123Ідентифікатор може потрапити в історію браузера, журнали сервера, аналітику або заголовок Referer.
HttpOnlySet-Cookie: session_id=abc123У цьому випадку cookie доступна JavaScript. Додайте HttpOnly для сесійної cookie.
SameSite=None без потребиSameSite=None дозволяє міжсайтове надсилання cookie. Якщо cross-site сценарій не потрібен, використовуйте Lax або Strict.
HttpOnly не шифрує cookie. Для захисту під час передавання потрібні HTTPS і Secure.
Cookie надсилається з багатьма запитами, тому вона має бути компактною. Дані в cookie також не можна вважати секретними, якщо cookie не захищена шифруванням і правильними атрибутами.
Краще зберігати в cookie лише випадковий ідентифікатор сесії, а дані — на сервері.
Приховування кнопки в інтерфейсі не є контролем доступу. Кожен захищений маршрут повинен перевіряти сесію та права на сервері.
GET для зміни стануЗапит GET не повинен видаляти акаунт, змінювати пароль або переказувати кошти. Для таких дій використовуйте POST, PUT, PATCH або DELETE разом із CSRF-захистом.
Якщо cookie створена з Path=/, а під час видалення використано інший Path, браузер може не видалити початкову cookie.
DomainCookie для .example.com потенційно доступна всім сумісним піддоменам. Якщо один із піддоменів буде скомпрометовано, це може вплинути на безпеку сесії.
Базовий варіант:
Set-Cookie: __Host-session_id=<random-value>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600Що тут забезпечують атрибути:
__Host- — обмежує неправильне використання домену та шляху;
HttpOnly — приховує cookie від JavaScript;
Secure — дозволяє передавання лише через HTTPS;
SameSite=Lax — зменшує ризик CSRF;
Path=/ — cookie доступна для всього застосунку;
Max-Age=3600 — сесія завершується через одну годину.
Точні значення залежать від архітектури застосунку. Наприклад, для cross-site інтеграції може знадобитися SameSite=None; Secure, але тоді особливо важливими стають CSRF-токени та перевірка Origin.
Cookie зберігається браузером і автоматично надсилається серверу.
Для сесії краще зберігати в cookie випадковий ідентифікатор, а не дані користувача.
Secure дозволяє надсилати cookie лише через HTTPS.
HttpOnly забороняє доступ до cookie з JavaScript.
SameSite обмежує міжсайтове надсилання cookie та допомагає захищатися від CSRF.
SameSite=None потребує Secure.
Для небезпечних операцій потрібен додатковий CSRF-захист.
Ідентифікатор сесії повинен бути випадковим, довгим і непередбачуваним.
Після входу ідентифікатор сесії варто змінювати.
Авторизацію та права доступу завжди перевіряє сервер.
Практичний production-варіант зазвичай використовує HttpOnly, Secure, SameSite=Lax, Path=/ і короткий термін життя.