20 лютого 2024 р.

Файли cookies, document.cookie

Файли cookies («куки») – це невеликі рядки з даними, які зберігаються безпосередньо в браузері. Вони є частиною HTTP протоколу, визначеного специфікацією RFC 6265.

Файли cookies зазвичай встановлюються вебсервером за допомогою заголовка HTTP Set-Cookie. Потім браузер автоматично додаватиме їх при (майже) кожному запиті до відповідного домену використовуючи заголовок HTTP Cookie.

Одним з найбільш поширених випадків використання є аутентифікація:

  1. При вході в систему, сервер використовує відповідь отриману з заголовка HTTP Set-Cookie, щоб додати в файл cookie унікальний “ідентифікатор сесії” .
  2. Наступного разу, коли на той самий домен буде відправлено запит, браузер надішле файл cookie використовуючи заголовок HTTP Cookie.
  3. Таким чином, сервер знає, хто зробив запит.

Також ми маємо доступ до файлів cookies з браузера, використовуючи властивість document.cookie.

У файлах cookies та їхніх атрибутах є багато тонкощів. В цьому розділі ми детально їх розглянемо.

Зчитування з document.cookie

Чи зберігає браузер файли cookie з цього сайту? Перевіримо:

// На javascript.info, ми використовуємо Google Analytics для збору статистики,
// тому тут мають бути деякі файли cookies
alert( document.cookie ); // cookie1=value1; cookie2=value2;...

Значення document.cookie складається з пар name=value розділених ;. Кожна пара – це окремий файл cookie.

Щоб знайти певний файл cookie потрібно розділити значення document.cookie за допомогою ;, а потім знайти потрібний ключ за його назвою. Для цього можна використовувати як регулярні вирази, так і функції для роботи з масивами.

Залишимо це завдання для самостійної роботи читача. Окрім того, в кінці розділу ви знайдете допоміжні функції для роботи з файлами cookies.

Запис в document.cookie

Ми можемо записувати в document.cookie. Але це не просто властивості даних, це аксесори (гетери/сетери). Присвоєння цих властивостей обробляється особливим чином.

Запис в document.cookie оновлює лише вказаний файл cookie, та не чіпатиме решту.

Наприклад, цей виклик встановить файл cookie з іменем user та значенням John:

document.cookie = "user=John"; // оновити лише файл cookie під назвою 'user'
alert(document.cookie); // показати всі файли cookies

Якщо ви запустите цей код, то, швидше за все, побачите декілька файлів cookies. Це тому, що операція document.cookie= оновлює не всі файли cookies, а лише вказаний файли з іменем user.

Технічно, ім’я та значення можуть містити будь-які символи. Щоб зберегти правильне форматування, вони повинні бути кодовані за допомогою вбудованої функції encodeURIComponent.

// спеціальні символи (пробіли, кирилиця), потребують кодування
let name = "my name";
let value = "John Smith";

// кодування cookie як my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);

alert(document.cookie); // ...; my%20name=John%20Smith
Обмеження

Є декілька обмежень:

  • За допомогою document.cookie, ви можете встановити/оновити лише один файл cookie.
  • Пара name=value, після кодування encodeURIComponent, не повинна перевищувати 4кБ. Тому ми не можемо зберігати великий обʼєм даних у файлах cookie.
  • Дозволена сумарна кількість файлів cookie на один домен приблизно 20+, точний ліміт залежить від браузера.

Файли cookies мають ряд атрибутів, деякі з них важливі і повинні бути задані.

Атрибути перераховуються після пари key=value та розділяються ;, як показано нижче:

document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"

domain

  • domain=site.com

Домен визначає звідки будуть доступні файли cookie. Проте, на практиці, існують деякі обмеження – ми не можемо вказати будь-який домен.

Не існує способу зробити файли cookie доступними з іншого домену 2-го рівня, тому other.com ніколи не отримає файл cookie заданий на site.com.

Це обмеження задля безпеки, воно дозволяє нам зберігати конфіденційні дані в файлах cookie, які будуть доступними лише на одному сайті.

Типово, файли cookie доступні лише на тому домені на якому були встановлені.

Зверніть увагу, що типово файли cookie також не доступні і на піддомені, такому як forum.site.com.

// якщо задати файл cookie на вебсайті site.com ...
document.cookie = "user=John"

// ...ми не побачимо їх на forum.site.com
alert(document.cookie); // користувача немає

…Проте цю поведінку можна змінити. Якщо ми хочемо дозволити піддоменам на кшталт forum.site.com отримувати файли cookie задані на site.com – це також можливо.

Для цього, встановлюючи файл cookie за адресою site.com, ми повинні явно задати в атрибуті domain кореневий домен: domain=site.com. Тоді всі піддомени побачать такий файл cookie.

Наприклад:

// на site.com
// зробити файл cookie доступним на будь-якому піддомені *.site.com:
document.cookie = "user=John; domain=site.com"

// пізніше

// на forum.site.com
alert(document.cookie); // файл cookie user=John існує
Застарілий синтаксис

Історично склалося так що, domain=.site.com(з крапкою перед site.com) спрацює так само, надаючи доступ до файлів cookie з піддоменів. Тепер крапки на початку доменних імен ігноруються, але деякі браузери можуть відмовитися встановлювати файл cookie, що містить такі крапки.

Отже, атрибут domain робить файли cookie доступними на піддоменах.

path

  • path=/mypath

URL-префікс адреси повинен бути абсолютним. Для того щоб файли cookie були доступними зі сторінок за цією адресою. Типово, це поточна сторінка.

Якщо в файлі cookie задано path=/admin, то він видимий на сторінках /admin та /admin/something, але не на /home або /adminpage.

Зазвичай, щоб зробити файл cookie доступним з усіх сторінок сайту, нам необхідно вказати корінь: path=/. Якщо цей атрибут не встановлено, типове значення обчислюється за допомогою цього методу.

expires, max-age

Типово, якщо файл cookie не має одного з цих атрибутів, то файл зникає при закриванні браузера/вкладки. Такі файли cookies називаються сесійними (“session cookies”).

Щоб файли cookies могли “пережити” закривання браузера/вкладки, можна встановити значення одного з атрибутів expires або max-age. max-age має перевагу, якщо встановлено обидва значення.

  • expires=Tue, 19 Jan 2038 03:14:07 GMT

Термін придатності файлу cookie визначає час, коли браузер автоматично видалить його (відповідно до часового поясу браузера).

Дата має бути вказана саме в такому форматі, в часовому поясі GMT. Щоб отримати правильну дату, можна скористатися date.toUTCString. Наприклад, можемо встановити, що термін придатності файлу cookie закінчується через 1 день:

// +1 день від сьогодні
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;

Якщо встановити значенням параметру expires дату з минулого, то файл cookie видалиться.

  • max-age=3600

Це альтернатива expires, який вказує термін придатності cookie в секундах з поточного моменту.

Як встановлено 0 або від’ємне значення, то файл cookie буде видалено:

// файл cookie буде видалено через 1 годину
document.cookie = "user=John; max-age=3600";

// видалити файл cookie (термін придатності закінчується прямо зараз)
document.cookie = "user=John; max-age=0";

secure

  • secure

Файл cookie повинен передаватися виключно по HTTPS-протоколу.

За замовчуванням, якщо ми створимо файл cookie на http://site.com, тоді він автоматично з’явиться на https://site.com та навпаки.

Тобто файли cookie базуються на домені, вони не залежать від протоколів.

В разі якщо цей атрибут задано, то файл cookie створений на https://site.com не буде доступний на тому ж вебсайті з HTTP-протоколом http://site.com. Тому якщо файли cookie містять конфіденційні дані, які в жодному разі не мають бути відправлені по незашифрованому HTTP-протоколу, тоді параметр secure це правильний вибір.

// припустимо, що зараз ми на https://
// створимо захищений файл cookie (доступний лише по HTTPS)
document.cookie = "user=John; secure";

samesite

Це ще один атрибут безпеки. Він створений щоб захищати від так званих XSRF-атак (міжсайтова підміна запиту).

Щоб зрозуміти як він працює та в яких випадках може бути корисним, давайте детальніше розглянемо поняття XSRF-атак.

XSRF-атаки

Уявіть, що ви увійшли в свій обліковий запис на сайті bank.com. Тобто: у вас є файли cookie з даними аутентифікації від цього сайту. Ваш браузер відправляє їх сайту bank.com при кожному запиті, для того щоб той розпізнав вас та виконав всі конфіденційні фінансові операції.

Тепер, переглядаючи вебсторінки в іншому вікні, ви випадково потрапили на сайт evil.com. Цей сайт містить код JavaScript, який відправляє форму <form action="https://bank.com/pay"> на bank.com з заповненими полями, які ініціюють транзакцію на рахунок хакера.

Браузер відправляє файли cookies щоразу як ви відвідуєте сайт bank.com, навіть якщо форма була відправлена з evil.com. Тому банк “впізнає” вас та дійсно виконає платіж.

Така атака називається міжсайтова підміна запиту (Cross-Site Request Forgery, XSRF).

Звісно, справжні банкові системи захищені від цього. У всіх згенерованих сайтом bank.com формах є спеціальне поле, так званий “токен захисту від XSRF”, який не може бути ані згенерований зловмисним сайтом, ані вилучений з віддаленої сторінки. Зловмисник може спробувати відправити форму туди, але не може отримати дані назад. Сайт bank.com перевіряє кожну отриману форму на наявність даного токену.

Проте такий захист вимагає більше часу на реалізацію. Нам потрібно переконатися, що кожна форма несе в собі обов’язкове поле з токеном, окрім того потрібно перевіряти всі запити.

Використання атрибуту samesite в файлі cookie

Атрибут samesite пропонує ще один спосіб захисту від атак, який (в теорії) не вимагає “токен захисту від xsrf”.

В нього є два можливі значення:

  • samesite=strict (те саме, що samesite без заданого значення)

Файли cookie з параметром samesite=strict ніколи не відправляються якщо користувач прийшов ззовні (з іншого сайту).

Іншими словами, якщо користувач перейшов за посиланням зі свого електронного листа або відправив форму з evil.com, або виконав яку завгодно операцію, що походить з іншого домену, файли cookie не відправляться.

Якщо файл cookie аутентифікації має атрибут samesite=strict, тоді у XSRF-атаки немає шансу на успіх, тому що запит з сайту evil.com надходить без файлу cookie. Таким чином, bank.com не ідентифікує користувача та не здійснить платіж.

Захист доволі надійний. Файли cookie з атрибутом samesite=strict відправлятимуться тільки якщо операції надходять з bank.com, наприклад відправлення форми з іншої сторінки домену bank.com.

Хоча, у цього рішення є дрібний недолік.

Коли користувач переходить за легітимним посилання до bank.com, як то зі своїх нотаток, то він буде неприємно здивований коли bank.com не “впізнає” його. Дійсно, в такому випадку файл cookie з атрибутом samesite=strict не відправляється.

Цю проблему можна вирішити використанням двох файлів cookie: один для загальної ідентифікації, лише для того щоб сказати: “Привіт, Іван”, а інший з параметром samesite=strict для операцій з даними. Тоді особа, яка перейшла за посиланням поза межами сайту побачить привітання, проте для того щоб другий файл cookie відправився, платіжні операції повинні бути ініційовані з сайту банку.

  • samesite=lax (те саме, що samesite без заданого значення)

Спрощене рішення яке так само захищає від XSRF-атак, але не руйнує очікування користувача.

Режим lax, так само як strict, забороняє браузеру відправляти файл cookies коли запит приходить не з сайту, але є одне виключення.

Файл cookie з параметром samesite=lax відправляється лише тоді, коли виконуються обидві умови:

  1. Обраний HTTP-метод безпечний (наприклад GET, але не POST)

    Повний список безпечних HTTP-методів зібрано в специфікації RFC7231. По суті, безпечними вважаються методи які використовуються для читання, а не для запису даних. Вони не повинні виконувати жодних операцій редагування даних. Перехід за посиланням це завжди метод GET, тобто безпечний.

  2. Операція виконує навігацію вищого рівня (змінює URL в адресному полі браузера)

    Як правило, ця умова виконується, проте якщо навігація відбувається в <iframe> то це не вищий рівень. Також, методи JavaScript для мережевих запитів не виконують навігації, тому вони не підходять.

Тому насправді, все що робить атрибут samesite=lax – це дозволяє найбільш поширеним операціям (таким як перехід по URL) передавати файли cookie. Наприклад, відкривання вебсайту за посиланням зі своїх нотаток, що повністю задовольняє умовам.

Але будь-яка більш складна операція, як то мережевий запит з іншого сайту чи відправлення форми, втрачає файли cookies.

Якщо вас це влаштовує, тоді використання samesite=lax скоріше за все не зіпсує враження користувача від взаємодії з вашим сайтом та захистить дані.

В цілому, samesite – це чудовий параметр.

Є лише один недолік:

  • samesite ігнорується (не підтримується) старими версіями браузерів, до 2017 року і раніше.

Тому, якщо для забезпечення захисту, покладатися виключно на атрибут samesite, то старі браузери залишаться вразливими.

Звісно, ми можемо використовувати samesite разом з іншими засобами безпеки, такими як “токен захисту від xsrf”, щоб додати ше один рівень захисту, а потім в майбутньому, коли старі браузери вимруть, ми зможемо відмовитися від токенів.

httpOnly

Цей атрибут не має нічого спільного з JavaScript, але ми повинні згадати його заради повноти картини.

Вебсервер використовує заголовок Set-Cookie щоб задати файли cookie. Також він може встановити атрибут httpOnly.

Даний атрибут забороняє будь-який доступ до файлів cookie з JavaScript. Ми не можемо бачити чи маніпулювати файлами cookie користуючись document.cookie.

Його використовують як запобіжний метод, щоб захистити від деяких атак, коли хакер вживляє на сторінку свій власний код JavaScript та чекає поки користувач зайде на таку сторінку. За ідеальних умов це взагалі неможливо, хакери не повинні мати можливості проникнути своїм кодом на наш сайт, але в коді можуть бути помилки, які все ж дозволяють їм це зробити.

Зазвичай, якщо таке трапляється, і користувач все ж зайшов на вебсторінку с хакерським кодом JavaScript, тоді код виконується та отримує доступ до команди document.cookie, яка містить аутентифікаційну інформацію. Це погано.

Проте якщо файл cookie має параметр httpOnly, то в такому разі document.cookie не бачить його, тому файл в безпеці.

Додаток: Функції файлів cookie

Тут наведено невелику підбірку функцій для роботи з файлами cookie, більш зручних аніж ручна модифікація document.cookie.

Існує безліч бібліотек для роботи з файлами cookie, тому ці функції тут скоріше в демонстраційних цілях. Але повністю робочі.

getCookie(name)

Найкоротший шлях щоб отримати доступ до файлів cookie це використання регулярних виразів.

Функція getCookie(name) повертає файл cookie з заданим іменем name:

// повертає файл cookie з заданим іменем
// або undefined якщо нічого не знайдено
function getCookie(name) {
  let matches = document.cookie.match(new RegExp(
    "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
  ));
  return matches ? decodeURIComponent(matches[1]) : undefined;
}

Тут динамічно генерується new RegExp який задовольняє ; name=<value>.

Зверніть увагу, що значення файлу cookie закодоване, тому getCookie використовує вмонтовану функцію decodeURIComponent щоб розшифрувати його.

setCookie(name, value, attributes)

Встановлює файл cookie з заданим ім’ям name, значенням value, та типовим параметром path=/ (можна редагувати, щоб додати інші типові значення):

function setCookie(name, value, attributes = {}) {

  attributes = {
    path: '/',
    // за потреби додайте інші типові значення
    ...attributes
  };

  if (attributes.expires instanceof Date) {
    attributes.expires = attributes.expires.toUTCString();
  }

  let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);

  for (let attributeKey in attributes) {
    updatedCookie += "; " + attributeKey;
    let attributeValue = attributes[attributeKey];
    if (attributeValue !== true) {
      updatedCookie += "=" + attributeValue;
    }
  }

  document.cookie = updatedCookie;
}

// Приклад використання:
setCookie('user', 'John', {secure: true, 'max-age': 3600});

deleteCookie(name)

Щоб видалили файли cookie, ми можемо викликати функцію вказавши від’ємне значення в параметрі max-age:

function deleteCookie(name) {
  setCookie(name, "", {
    'max-age': -1
  })
}
Операції оновлення або видалення повинні використовувати ті самі адресу та домен

Зауважте: коли ми оновлюємо чи видаляємо файли cookie, ми повинні використовувати ті самі адресу та доменне ім’я, що і при встановленні.

Все разом: cookie.js.

Додаток: Сторонні файли cookies

Файли cookie називаються “сторонніми”, якщо вони розміщені з домену який відрізняється від того, до якого належить поточна сторінка.

Наприклад:

  1. Сторінка розміщена на site.com завантажує банер з іншого сайту: <img src="https://ads.com/banner.png">.

  2. Разом з банером, віддалений сервер на ads.com може встановити заголовок Set-Cookie зі значенням id=1234 взятим з файлу cookie. Створені файли cookie походять з домену ads.com та будуть видимі лише з ads.com:

  3. Наступного разу коли ads.com відправить запит на доступ, віддалений сервер отримає id з файлу cookie та розпізнає користувача:

  4. Іще важливіше те, що коли користувач переходить з site.com до іншого сайту other.com, на якому також є банер, тоді ads.com отримує файли cookie, так наче вони насправді належать ads.com і таким чином розпізнає відвідувача та відстежить його, коли той переміщається між сайтами:

Сторонні файли cookie традиційно використовуються для відстеження статистики відвідувань та показу реклами. Вони прив’язані до початкового домену, тому ads.com може відстежувати одного й того ж користувача при переході між сайтами, якщо всі вони мають до нього доступ.

Природно, що деякі люди не хочуть щоб за ними стежили, тому браузери дозволяють їм відключити такі файли cookie.

Крім того, деякі сучасні браузери запроваджують спеціальну політику для таких файлів cookie:

  • Safari взагалі не дозволяє сторонні файли cookie.
  • У Firefox є “чорний список” сторонніх доменів, з яких він блокує надходження сторонніх файлів cookie.
Будь ласка, зверніть увагу:

Якщо ми завантажимо скрипт з стороннього домену, такого як <script src="https://google-analytics.com/analytics.js">, і цей скрипт використовує document.cookie щоб встановити файли cookie, то вони не вважатимуться сторонніми.

Якщо скрипт встановлює файл cookie, тоді неважливо звідки завантажений цей скрипт – файл cookie належить домену поточного web-сайту.

Додаток: GDPR

Ця тема взагалі не пов’язана з JavaScript, це просто потрібно пам’ятати, налаштовуючи файли cookie.

У Європі існує законодавство під назвою GDPR, яке встановлює набір правил для вебсайтів щодо поваги до конфіденційності користувачів. Одне з цих правил — вимагати від користувача явного дозволу на відстеження файлів cookie.

Зауважте, що йдеться лише про відстеження/ідентифікацію/авторизацію файлів cookies.

Отже, якщо ми встановлюємо файл cookie, який лише зберігає деяку інформацію, але не відстежує та не ідентифікує користувача, ми можемо це зробити.

Але якщо ми збираємося встановити файл cookie з даними аутентифікації або ідентифікатором відстеження, користувач повинен дозволити це.

Зазвичай, виконання GDPR web-сайтами реалізується в двох варіантах. Ви, напевно, вже бачили їх обидва в мережі інтернет:

  1. Якщо вебсайт хоче встановити відстежувальні файли cookie лише для аутентифікованих користувачів.

    Для цього в реєстраційній формі має бути прапорець на кшталт “прийняти політику конфіденційності” (яка описує, як використовуються файли cookie), користувач повинен відмітити його, лише тоді вебсайт може встановлювати файли cookie для авторизації.

  2. Якщо вебсайт хоче встановити відстежувальні файли cookies для всіх.

    Щоб зробити це законно, вебсайт показує спливаюче модальне вікно для нових користувачів і вимагає від них погодитися на файли cookie. Тоді вебсайт може встановлювати їх і дозволити людям бачити контент. Але це може турбувати нових відвідувачів. Ніхто не любить бачити такі спливаючі модальні вікна, які вимагають взаємодії, замість перегляду контенту. Але GDPR вимагає чіткої угоди.

GDPR стосується не лише файлів cookie, а й інших питань, пов’язаних із конфіденційністю, але це виходить за рамки даного розділу.

Підсумки

document.cookie забезпечує доступ до файлів cookies

  • Операції запису змінюють лише вказаний файл cookie.
  • Ім’я та значення файлу cookie повинні бути закодовані.
  • Один файл cookie не повинен перевищувати 4кБ, 20+ файлів на один сайт (залежно від браузера).

Атрибути:

  • path=/, встановлює поточну адресу як типове значення, робить файл cookie видимим лише за вказаною адресою.
  • domain=site.com, типово файли cookie видимі лише на поточному домені. Якщо домен вказано явно, файли cookie стають видимі на сторінках піддомену.
  • expires або max-age задають кінцевий термін придатності файлів cookie. Без них файли cookie помирають відразу після закриття вікна браузера.
  • secure робить файли cookie доступними лише по HTTPS (HTTPS-only).
  • samesite забороняє браузеру відправляти файли cookie у відповідь на запити які надходять з зовнішнього сайту. Це допомагає запобігти XSRF-атакам.

Додатково:

  • Браузери можуть забороняти сторонні файли cookie, наприклад Safari робить це типово. Також ведеться робота, щоб запровадити таке ж і в Chrome.
  • Встановлюючи відстежувальний файл cookie для громадян ЄС, GDPR вимагає запитувати дозвіл.
Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)