Файли Cookie (куки, реп'яшки) — це, мабуть, найстаріша та найвідоміша технологія зберігання даних на клієнті. Вони з нами з 1994 року, коли Лу Монтуллі (Lou Montulli) в Netscape вигадав спосіб запам'ятати користувача між запитами.
До появи cookies веб був "безстанівим" (stateless). Сервер не мав пам'яті. Кожен запит був як перший поцілунок — сервер не пам'ятав, хто ви і що ви робили секунду тому. Cookies змінили все, додавши "пам'ять" (state) до HTTP.
Можливо, ви думаєте: "У нас є LocalStorage, IndexedDB, навіщо мені ці старі cookies?"
HttpOnly). LocalStorage вразливий до XSS.SameSite, Secure, Domain, Path), яку критично важливо розуміти Fullstack розробнику.!NOTE Cookie — це просто рядок з даними (зазвичай
key=value), який зберігається в браузері і автоматично додається до заголовкаCookieпри кожному запиті до відповідного домену.
На низькому рівні, cookie — це частина HTTP протоколу (RFC 6265).
Сервер надсилає заголовок Set-Cookie у відповіді:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session_id=12345; Secure; HttpOnly; SameSite=Lax
Set-Cookie: user_pref=dark_mode; Max-Age=3600
При наступному запиті до цього ж домену, браузер збирає всі доступні cookies і відправляє їх в одному заголовку Cookie (розділені крапкою з комою):
GET /profile HTTP/1.1
Host: example.com
Cookie: session_id=12345; user_pref=dark_mode
У JavaScript доступ до cookies реалізований через "магічну" властивість document.cookie. Це не звичайний рядок об'єкта, а аксесор (get/set).
const allCookies = document.cookie
console.log(allCookies)
// Виведе щось на зразок: "user=John; session_id=xyz; theme=dark"
Проблеми при читанні:
name=value. Ми не бачимо атрибутів (expires, path, domain, secure).HttpOnly cookies (це фіча безпеки).Запис працює контрінтуїтивно. Коли ви присвоюєте значення document.cookie, ви не перезаписуєте всі cookies, а додаєте або оновлюєте лише одну конкретну cookie.
// Це НЕ видалить інші cookies! Це додасть нову.
document.cookie = 'user=John'
// Це оновить існуючу cookie 'user'
document.cookie = 'user=Jane'
Згідно зі специфікацією, ім'я та значення cookie можуть містити лише певний набір символів ASCII. Пробіли, коми, крапки з комою, кирилиця — заборонені (або можуть зламати парсинг).
Тому правило №1: Завжди кодуйте імена та значення!
const name = 'my name'
const value = 'Ivan & Oksana'
// ❌ Небезпечно:
// document.cookie = name + "=" + value;
// ✅ Правильно:
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value)
// Результат: my%20name=Ivan%20%26%20Oksana
Атрибути — це налаштування, які вказують браузеру, як поводитися з cookie (коли видаляти, кому відправляти). Вони вказуються після крапки з комою.
Expires та Max-AgeЗа замовчуванням (якщо атрибути не вказані), cookie є сесійною (Session Cookie). Вона живе, доки користувач не закриє браузер (саме браузер, а не вкладку!).
Щоб зробити cookie "вічною" (Persistent), треба вказати час життя.
expires (Old school)Вказує конкретну дату в форматі GMT (UTC), коли cookie помре.
// Живе до певної дати
document.cookie = 'user=John; expires=Tue, 19 Jan 2038 03:14:07 GMT'
Мінус: Треба самому форматувати дату (date.toUTCString()).
max-age (Modern)Вказує час життя в секундах від поточного моменту.
// Живе 1 годину (3600 сек)
document.cookie = 'user=John; max-age=3600'
// Живе 1 рік
document.cookie = 'user=John; max-age=31536000'
Не існує методу deleteCookie(). Щоб видалити cookie, треба встановити її знову з минулою датою або max-age=0 (або -1).
document.cookie = 'user=; max-age=0'
!IMPORTANT При видаленні (як і при оновленні) ви повинні вказати ті ж самі
pathіdomain, з якими cookie була створена! Інакше ви можете створити дублікат або нічого не видалити.
Path та DomainЦі атрибути контролюють, на які URL браузер буде відправляти cookie.
PathВказує URL-шлях, для якого актуальна cookie.
path=/ (стандарт): доступна на всьому сайті.path=/admin: доступна на /admin, /admin/users, але не на / чи /home.// Видно тільки в адмінці
document.cookie = 'admin_token=123; path=/admin'
DomainНайскладніший атрибут. Він контролює доступність між піддоменами.
Правило: Cookie, встановлена на домені, ніколи не доступна на іншому домені другого рівня (наприклад site.com не бачить cookies google.com).
Але як щодо app.site.com та blog.site.com?
domain (HostOnly):
Якщо ви пишете document.cookie = "a=1", cookie прив'язується тільки до поточного хоста.app.site.com.site.com.blog.site.com.www.app.site.com.domain=site.com:
Якщо ви явно вказуєте домен, cookie стає доступною на цьому домені і всіх піддоменах.
document.cookie = "a=1; domain=site.com"site.comapp.site.comdev.api.site.comSecuredocument.cookie = 'user=John; Secure'
Атрибут Secure каже браузеру: "Ніколи не відправляй цю cookie по незахищеному HTTP з'єднанню". Вона полетить на сервер тільки якщо протокол HTTPS.
Це захищає від атак типу Man-in-the-Middle, коли зловмисник перехоплює трафік у відкритому Wi-Fi.
!NOTE На
localhostбраузери роблять виняток і дозволяютьSecurecookies навіть по http, щоб розробникам було зручно.
HttpOnlyЦей атрибут неможливо встановити через JavaScript (document.cookie). Він встановлюється тільки сервером через заголовок Set-Cookie.
Що він робить?
Він забороняє JavaScript читати цю cookie. document.cookie її просто не побачить.
Навіщо?
Це головний захист від XSS (Cross-Site Scripting). Якщо хакер знайде вразливість на вашому сайті і запустить свій JS код:
alert(document.cookie) — він не отримає ваші сесійні токени, якщо вони HttpOnly.
!TIP > Best Practice: Усі cookies, що відповідають за аутентифікацію (JWT, session ID), мусять бути
HttpOnly.
SameSiteАтрибут SameSite був введений для боротьби з CSRF (Cross-Site Request Forgery) та відстеженням. Він контролює, чи відправляється cookie, коли запит ініційовано з іншого сайту (cross-site).
Є три значення: Strict, Lax, None.
bank.com). Cookie сесії є в браузері.evil.com.evil.com є прихована форма, яка відправляє POST запит на bank.com/transfer (переказати гроші).bank.com і (за старими правилами) автоматично додає cookie.SameSite=StrictCookie відправляється тільки для First-Party запитів (коли ви вже знаходитесь на bank.com).
Якщо ви перейшли за посиланням з Facebook на ваш банк, cookie не відправиться. Ви будете розлогінені.
Це супер безпечно, але незручно.
SameSite=Lax (Золотий стандарт 🛡️)Це налаштування за замовчуванням у сучасних браузерах. Воно забезпечує найкращий баланс між безпекою та зручністю користувача.
Логіка роботи: Cookie відправляється на сервер тільки якщо виконуються дві умови:
GET).Матриця поведінки:
| Сценарій | Тип запиту | Cookie відправляється? | Пояснення |
|---|---|---|---|
🔗 Клік по посиланню (<a href="...">) | GET (Navigation) | ✅ ТАК | Користувач свідомо переходить на сайт. Логін зберігається. |
🖼️ Завантаження ресурсу (<img>, <iframe>, <script>) | GET (Subresource) | ❌ НІ | Це фоновий запит з іншого сайту. Захищає від трекінгу та CSRF. |
📝 Відправка форми (<form method="POST">) | POST | ❌ НІ | POST — метод, що змінює дані. Блокування захищає від класичних CSRF атак. |
| 📡 Fetch / XHR | GET/POST | ❌ НІ | Cross-site AJAX запити ніколи не передають Lax куки. |
| 🔄 window.location.href | GET (Navigation) | ✅ ТАК | Програмна навігація прирівнюється до кліку по посиланню. |
Візуалізація захисту (Mermaid):
flowchart TD
User((Користувач))
Evil[Сайт хакера<br/>attacker.com]
Bank[Ваш сайт<br/>bank.com]
User --> Evil
subgraph Browser["Браузер (Cookie: SameSite=Lax)"]
direction TB
Link["1. Клік по посиланню<br/><a href='bank.com'>"]
Img["2. Прихований запит<br/><img src='bank.com/pay'>"]
Form["3. Авто-форма<br/><form method='POST'>"]
end
Evil -.-> Link
Evil -.-> Img
Evil -.-> Form
Link -->|GET Navigation| Allowed{✅ Дозволено}
Img -->|Subresource| Blocked{❌ Блоковано}
Form -->|POST Method| Blocked
Allowed -->|Cookie Included| Bank
Blocked -->|No Cookie| Bank
style Allowed fill:#d4edda,stroke:#28a745,color:black
style Blocked fill:#f8d7da,stroke:#dc3545,color:black
!NOTE Завдяки
SameSite=Lax, якщо ви залогінені в Facebook і перейдете на нього з іншого сайту по посиланню — ви залишитесь залогіненим. Але якщо інший сайт спробує лайкнути пост за вас через прихований запит — куки не передадуться, і Facebook відхилить дію.
SameSite=NoneПоведінка "як раніше". Cookie відправляється завжди, навіть у запитах сторонніх ресурсів (рекламні трекери, YouTube embed і т.д.).
Вимога: Якщо SameSite=None, ви зобов'язані додати Secure. Без HTTPS така cookie буде відхилена браузером.
| Значення | Перехід по посиланню | AJAX/Fetch/Form POST | Iframe |
|---|---|---|---|
| Strict | ❌ | ❌ | ❌ |
| Lax | ✅ (Safe methods GET) | ❌ | ❌ |
| None | ✅ | ✅ | ✅ |
Partitioned (CHIPS)Google Chrome планує повністю заблокувати сторонні cookies (SameSite=None). Але як бути сервісам типу "чат підтримки", які вбудовуються в тисячі сайтів через iframe і потребують збереження стану?
Рішення — CHIPS (Cookies Having Independent Partitioned State).
Атрибут Partitioned каже браузеру: "Зберігай цю cookie окремо для кожного сайту, де вона використовується".
Cookie від chat.com на сайті site-a.com буде відрізнятися від cookie chat.com на site-b.com. Вони не зможуть "спілкуватися" між собою, але зможуть зберігати налаштування в межах одного сайту.
Set-Cookie: user_color=blue; SameSite=None; Secure; Partitioned
Це коли зловмисник впроваджує свій JS код на вашу сторінку.
HttpOnly для сесійних cookies.Це коли зловмисник змушує браузер виконати дію від вашого імені.
SameSite=Lax (базовий захист).Атака, коли зловмисник контролює піддомен (наприклад useless.site.com), встановлює cookie з domain=site.com, і ця cookie "перебиває" легітимну cookie на app.site.com.
__Host-: вимагає Secure, Path=/ і відсутність Domain.__Secure-: вимагає Secure.
Приклад: __Host-SessionID=123. Браузер гарантує, що вона встановлена безпечно.В Європі (GDPR) та Каліфорнії (CCPA) ви не можете просто так ставити cookies.
Ви не повинні завантажувати скрипти аналітики (Google Analytics, Facebook Pixel), доки користувач не натисне "Accept". Багато сайтів порушують це, ставлячи cookies відразу, а банер показують "для галочки". Це незаконно в ЄС.
CookieUtilsОскільки рідний API document.cookie жахливий, напишемо надійну бібліотеку для роботи з cookies. Це готовий до продакшену код.
/**
* Утиліти для роботи з Cookies.
* Підтримує всі сучасні атрибути, правильне кодування та парсинг.
*/
const CookieUtils = {
/**
* Отримати cookie за ім'ям
* @param {string} name
* @returns {string|undefined} Значення або undefined
*/
get(name) {
if (typeof document === 'undefined') return undefined // SSR guard
const matches = document.cookie.match(
new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)'),
)
return matches ? decodeURIComponent(matches[1]) : undefined
},
/**
* Встановити cookie
* @param {string} name
* @param {string} value
* @param {Object} options - { expires, path, domain, secure, samesite }
*/
set(name, value, options = {}) {
options = {
path: '/', // за замовчуванням доступна на всьому сайті
// ...інші дефолти
...options,
}
if (options.expires instanceof Date) {
options.expires = options.expires.toUTCString()
}
let updatedCookie = encodeURIComponent(name) + '=' + encodeURIComponent(value)
for (let optionKey in options) {
updatedCookie += '; ' + optionKey
let optionValue = options[optionKey]
if (optionValue !== true) {
updatedCookie += '=' + optionValue
}
}
document.cookie = updatedCookie
},
/**
* Видалити cookie
* @param {string} name
* @param {Object} options - Важливо передати ті ж path/domain, що при створенні!
*/
delete(name, options = {}) {
this.set(name, '', {
...options,
'max-age': -1,
})
},
/**
* Перевірити чи є cookies увімкненими в браузері
*/
isEnabled() {
try {
document.cookie = 'cookietest=1'
const ret = document.cookie.indexOf('cookietest=') !== -1
this.delete('cookietest')
return ret
} catch (e) {
return false
}
},
}
// --- Приклади використання ---
// 1. Встановити сесійну cookie
CookieUtils.set('user', 'John')
// 2. Встановити cookie на 1 день з Secure
CookieUtils.set('token', 'xyz123', {
'max-age': 86400,
secure: true,
samesite: 'Strict',
})
// 3. Отримати
console.log(CookieUtils.get('token')) // 'xyz123'
// 4. Видалити
CookieUtils.delete('user')
Що краще використовувати?
| Характеристика | Cookies | LocalStorage | SessionStorage |
|---|---|---|---|
| Обсяг | 4 KB | 5-10 MB | 5-10 MB |
| Відправка на сервер | Автоматично (з кожним запитом) | Ні (руками в JS) | Ні |
| Термін життя | Налаштовується (expires) | Вічно (до очищення) | До закриття вкладки |
| Доступність (вікна) | Всі вкладки/вікна | Всі вкладки/вікна | Тільки поточна вкладка |
| Доступ JS | Так (крім HttpOnly) | Так | Так |
| Безпека (XSS) | Можна захистити (HttpOnly) | Вразливе | Вразливе |
| Використання | Auth токени, серверні сесії | Налаштування UI, кеш, чернетки | Дані форми, скрол |
!WARNING Ніколи не зберігайте JWT токени в LocalStorage, якщо ви хочете максимальної безпеки. XSS атака дозволить вкрасти токен з LocalStorage миттєво. HttpOnly Cookies — єдине місце, куди JS не може залізти.
encodeURIComponent для імен і значень.Secure та SameSite=Lax.document.cookie регулярками вручну в кожному файлі. Використовуйте js-cookie або наш CookieUtils.Тепер ви знаєте про cookies більше, ніж 90% розробників. Ви готові до світу без сторонніх cookies! 🍪
HttpOnly
Secure
SameSite
4KB Limit
Відновлюване завантаження файлів
Як реалізувати надійне завантаження великих файлів з можливістю відновлення після втрати з'єднання
js-cookie: Керування Cookies без Болю
У попередніх розділах ми дослідили нативний інтерфейс document.cookie. Ви напевно помітили, наскільки він архаїчний та незручний. Робота з ним нагадує спробу написати SMS, використовуючи азбуку Морзе — це можливо, але навіщо так страждати у 21-му столітті?