Пошук уроків, статей та іншого контенту
Порівняєте localStorage, sessionStorage, cookies та IndexedDB і виберете безпечне місце для різних даних.
Браузер може зберігати дані між відкриттями сторінки або передавати їх серверу під час HTTP-запитів. Для цього найчастіше використовують:
localStorage;
sessionStorage;
cookies;
IndexedDB.
Ці механізми мають різне призначення, обмеження та рівень ризику. Неправильний вибір може призвести до:
витоку персональних даних;
крадіжки токенів автентифікації;
передачі зайвих даних на сервер;
помилок у паралельних вкладках;
повільної роботи інтерфейсу.
Головне правило:
Жодне сховище, доступне JavaScript-коду сторінки, не захищає дані від шкідливого JavaScript, який уже виконується в контексті вашого сайту.
Тому localStorage, sessionStorage та IndexedDB не підходять для довготривалого зберігання секретів, якщо сторінка має вразливість XSS.
Доступ до сховищ обмежується політикою same-origin. Origin складається зі:
протоколу, наприклад https
домену;
порту.
Наприклад, ці адреси мають різні origin:
https://example.com;
http://example.com;
https://api.example.com;
https://example.com:8443.
Скрипт із одного origin не може довільно читати сховища іншого origin.
Однак same-origin не означає повну безпеку:
XSS-код на вашому origin може читати localStorage, sessionStorage та IndexedDB;
cookies без прапорця HttpOnly також доступні через JavaScript;
розширення браузера або шкідливе програмне забезпечення можуть мати окремі привілеї;
дані у браузері можуть бути видалені користувачем або самим браузером.
localStoragelocalStorage — це просте сховище пар ключ-значення, яке зазвичай зберігається після закриття вкладки та перезапуску браузера.
Значення завжди мають тип рядка:
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");
console.log(theme); // "dark"
localStorage.removeItem("theme");Для об’єктів потрібно явно використовувати JSON:
const settings = {
language: "uk",
notifications: true
};
localStorage.setItem("settings", JSON.stringify(settings));
const savedSettings = localStorage.getItem("settings");
const parsedSettings = savedSettings
? JSON.parse(savedSettings)
: null;
console.log(parsedSettings);localStoragelocalStorage добре підходить для невеликих несекретних налаштувань:
вибрана тема;
мова інтерфейсу;
стан відображення бічної панелі;
прапорець, що користувач уже побачив підказку;
локальні параметри сортування.
localStorage має синхронний API. Операції блокують головний потік JavaScript, тому не варто зберігати в ньому великі обсяги даних.
Також:
дані доступні будь-якому JavaScript-коду на цьому origin;
значення не шифруються автоматично;
немає вбудованого терміну дії;
сховище не призначене для складних запитів;
обсяг залежить від браузера та його політик;
зміни в одній вкладці можуть вплинути на інші вкладки.
Не зберігайте в localStorage:
паролі;
секретні ключі;
refresh-токени;
довготривалі access-токени;
паспортні або платіжні дані;
будь-яку інформацію, витік якої створює серйозний ризик.
sessionStoragesessionStorage має той самий API, що й localStorage, але його життєвий цикл пов’язаний із вкладкою браузера.
sessionStorage.setItem("checkoutStep", "shipping");
const step = sessionStorage.getItem("checkoutStep");
console.log(step); // "shipping"
sessionStorage.removeItem("checkoutStep");Зазвичай дані зберігаються під час перезавантаження сторінки, але видаляються після закриття вкладки або вікна.
Окремі вкладки мають окремі області sessionStorage. Це зручно, коли один користувач може одночасно працювати з кількома незалежними формами або процесами.
sessionStorageПриклади:
поточний крок багатокрокової форми;
тимчасовий стан фільтрів;
ідентифікатор незавершеної локальної операції;
дані, які не повинні залишатися після завершення сесії вкладки.
sessionStorageВідсутність довготривалого зберігання зменшує час існування даних, але не захищає їх від XSS. Скрипт, який виконується у вашій вкладці, може прочитати sessionStorage так само, як і localStorage.
Cookie — це невеликий фрагмент даних, пов’язаний із доменом. На відміну від Web Storage, cookie може автоматично надсилатися серверу під час HTTP-запитів.
Сервер може встановити cookie заголовком Set-Cookie:
Set-Cookie: sessionId=random-value; Path=/; Secure; HttpOnly; SameSite=LaxJavaScript може працювати лише з cookies, які не мають прапорця HttpOnly:
document.cookie = "language=uk; Max-Age=604800; Path=/; Secure; SameSite=Lax";
console.log(document.cookie);document.cookie повертає рядок із доступними cookies, тому для аналізу його часто потрібно розібрати.
HttpOnlyCookie з HttpOnly недоступна через document.cookie.
Це важливий захист для сесійних ідентифікаторів:
Set-Cookie: sessionId=random-value; HttpOnly; Secure; SameSite=Lax; Path=/HttpOnly не робить cookie абсолютним захистом від XSS. Шкідливий скрипт не зможе прочитати значення cookie, але може спробувати виконувати запити від імені користувача, якщо застосунок не має інших захисних механізмів.
SecureCookie з Secure передається лише через HTTPS.
У робочому середовищі сесійні cookies повинні використовувати HTTPS і прапорець Secure.
SameSiteSameSite обмежує надсилання cookie у міжсайтових запитах.
Основні значення:
Strict — найсуворіший режим;
Lax — поширений компроміс для сесійних cookies;
None — cookie може надсилатися у міжсайтових контекстах, але тоді обов’язковий Secure.
Наприклад:
Set-Cookie: sessionId=random-value; HttpOnly; Secure; SameSite=Lax; Path=/SameSite допомагає зменшити ризик CSRF, але спосіб захисту потрібно узгоджувати з архітектурою застосунку.
Max-Age і ExpiresВони визначають час життя cookie:
Set-Cookie: rememberMe=true; Max-Age=2592000; Secure; SameSite=Lax; Path=/Сесійна cookie без Max-Age та Expires зазвичай видаляється після завершення сесії браузера. Поведінка може залежати від браузера та його налаштувань.
Domain і PathDomain визначає, для яких доменів доступна cookie. Надто широкий Domain збільшує область її дії.
Path обмежує шляхи, до яких cookie надсилається. Наприклад:
Set-Cookie: adminSession=value; HttpOnly; Secure; SameSite=Strict; Path=/adminЯкщо атрибут Domain не вказувати, cookie зазвичай є host-only і не поширюється на піддомени. Це часто безпечніший варіант.
Cookies особливо доречні, коли серверу потрібно автоматично отримувати дані:
ідентифікатор сесії;
короткотривалий маркер автентифікації;
налаштування, які сервер використовує під час формування відповіді;
технічні параметри сесії.
Для сесії зазвичай використовують непрозорий випадковий ідентифікатор у HttpOnly cookie. Стан сесії зберігається на сервері або в захищеному серверному сховищі.
IndexedDBIndexedDB — асинхронна вбудована база даних браузера. Вона підтримує:
великі обсяги структурованих даних;
об’єкти, масиви та інші значення, які підтримує structured clone;
індекси;
транзакції;
кілька object store.
IndexedDB підходить для складного офлайн-стану, а не лише для кількох налаштувань.
Нижче наведено повний приклад, який можна виконати в браузері. Він створює базу даних, зберігає чернетку нотатки та читає її назад.
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Чернетка в IndexedDB</title>
</head>
<body>
<textarea id="note" rows="8" cols="50"
placeholder="Введіть текст нотатки"></textarea>
<br>
<button id="save">Зберегти</button>
<button id="load">Завантажити</button>
<p id="status"></p>
<script>
const DB_NAME = "notes-app";
const DB_VERSION = 1;
const STORE_NAME = "drafts";
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.addEventListener("upgradeneeded", () => {
const database = request.result;
if (!database.objectStoreNames.contains(STORE_NAME)) {
database.createObjectStore(STORE_NAME, {
keyPath: "id"
});
}
});
request.addEventListener("success", () => {
resolve(request.result);
});
request.addEventListener("error", () => {
reject(request.error);
});
});
}
function saveDraft(draft) {
return openDatabase().then((database) => {
return new Promise((resolve, reject) => {
const transaction = database.transaction(
STORE_NAME,
"readwrite"
);
const store = transaction.objectStore(STORE_NAME);
store.put(draft);
transaction.addEventListener("complete", () => {
database.close();
resolve();
});
transaction.addEventListener("error", () => {
database.close();
reject(transaction.error);
});
});
});
}
function readDraft(id) {
return openDatabase().then((database) => {
return new Promise((resolve, reject) => {
const transaction = database.transaction(
STORE_NAME,
"readonly"
);
const request = transaction
.objectStore(STORE_NAME)
.get(id);
request.addEventListener("success", () => {
database.close();
resolve(request.result);
});
request.addEventListener("error", () => {
database.close();
reject(request.error);
});
});
});
}
const noteElement = document.querySelector("#note");
const statusElement = document.querySelector("#status");
document.querySelector("#save").addEventListener("click", async () => {
try {
await saveDraft({
id: "main",
text: noteElement.value,
updatedAt: new Date().toISOString()
});
statusElement.textContent = "Чернетку збережено";
} catch (error) {
statusElement.textContent = "Не вдалося зберегти чернетку";
console.error(error);
}
});
document.querySelector("#load").addEventListener("click", async () => {
try {
const draft = await readDraft("main");
if (draft) {
noteElement.value = draft.text;
statusElement.textContent = "Чернетку завантажено";
} else {
statusElement.textContent = "Чернетку не знайдено";
}
} catch (error) {
statusElement.textContent = "Не вдалося завантажити чернетку";
console.error(error);
}
});
</script>
</body>
</html>IndexedDBПриклади:
кеш даних для офлайн-режиму;
чернетки документів;
черга операцій для синхронізації із сервером;
локальний каталог товарів;
великі результати запитів;
історія змін, яку не потрібно надсилати серверу одразу.
IndexedDB не є сховищем секретів. Дані в ньому доступні JavaScript-коду вашого origin, тому XSS-вразливість залишається критичною.
Використовуйте localStorage, якщо налаштування мають переживати перезапуск браузера:
localStorage.setItem("colorScheme", "dark");Якщо дані потрібні лише в межах поточної вкладки, використовуйте sessionStorage:
sessionStorage.setItem("activeTab", "reports");sessionStorage зручний для тимчасового стану, але не покладайтеся на нього як на засіб безпеки. Користувач може змінити його через DevTools, а XSS-код — прочитати.
Для сесії вебзастосунку типовий безпечніший підхід:
сервер створює сесію;
сервер надсилає випадковий ідентифікатор у cookie;
cookie має HttpOnly, Secure і відповідний SameSite;
браузер автоматично надсилає cookie на дозволені запити;
сервер перевіряє сесію.
Не зберігайте довготривалі токени автентифікації у localStorage, якщо можна використати захищену cookie.
Якщо API використовує токен у заголовку Authorization, архітектура може вимагати зберігати токен у пам’яті JavaScript. Це зменшує час його життя після перезавантаження, але не усуває ризик XSS під час поточної роботи сторінки.
Для великих або структурованих даних використовуйте IndexedDB, а не localStorage.
localStorage зручний для простих значень, але не замінює базу даних.
Оскільки XSS є одним із головних ризиків для клієнтських сховищ, важливо:
не вставляти неперевірені дані через innerHTML;
використовувати textContent для звичайного тексту;
екранувати або санітизувати HTML, якщо його справді потрібно показувати;
застосовувати Content Security Policy;
не вставляти дані користувача в небезпечні контексти;
оновлювати залежності;
перевіряти дані на сервері, а не лише в браузері.
Небезпечний приклад:
messageElement.innerHTML = userMessage;Якщо userMessage містить HTML або JavaScript, це може створити XSS-вразливість.
Безпечніший варіант для звичайного тексту:
messageElement.textContent = userMessage;Навіть HttpOnly cookie не скасовує потребу захищати інтерфейс від XSS. Шкідливий скрипт може виконувати дії в межах відкритої сесії користувача.
Cookies автоматично додаються до запитів, тому сервер має враховувати ризик CSRF.
Для захисту можуть використовуватися:
SameSite=Lax або SameSite=Strict, якщо це сумісно з вимогами;
CSRF-токен для операцій, які змінюють дані;
перевірка заголовків Origin або Referer, коли це доречно;
коректна перевірка методу та маршруту на сервері;
заборона небезпечних операцій через GET.
Не вважайте, що перевірка лише на клієнті захищає сервер. Клієнтський код і всі значення у браузері можна змінити.
Будь-які дані зі сховища потрібно вважати ненадійними. Користувач може:
змінити їх у DevTools;
видалити їх;
записати значення неправильного формату;
підмінити числове значення рядком;
зберегти застарілу версію структури.
Під час читання перевіряйте формат і передбачайте помилки JSON:
function readSettings() {
const rawValue = localStorage.getItem("settings");
if (!rawValue) {
return {
language: "uk",
notifications: true
};
}
try {
const value = JSON.parse(rawValue);
if (
!value ||
typeof value !== "object" ||
(value.language !== "uk" && value.language !== "en") ||
typeof value.notifications !== "boolean"
) {
throw new Error("Некоректний формат налаштувань");
}
return value;
} catch {
localStorage.removeItem("settings");
return {
language: "uk",
notifications: true
};
}
}
console.log(readSettings());Для важливих правил доступу перевірка на клієнті не має значення. Сервер повинен повторно перевіряти права користувача.
Видаляйте тимчасові дані, коли вони більше не потрібні:
sessionStorage.removeItem("checkoutStep");
localStorage.removeItem("temporaryDraft");Для cookies потрібно встановити минулий час життя або нульовий Max-Age з тими самими параметрами області:
document.cookie =
"temporaryFlag=; Max-Age=0; Path=/; Secure; SameSite=Lax";Cookie з HttpOnly не можна видалити через JavaScript. Її повинен видалити сервер, надіславши відповідний Set-Cookie.
Для IndexedDB можна видалити окремий запис або всю базу:
indexedDB.deleteDatabase("notes-app");Не видаляйте всю базу без потреби: це може стерти важливі офлайн-дані користувача.
localStorageЦе поширене рішення, але XSS-код може прочитати токен і передати його зловмиснику.
Безпечніший типовий варіант для сесії — HttpOnly; Secure; SameSite cookie та серверна перевірка сесії.
sessionStorage захищенимsessionStorage лише обмежує час життя та область доступу. Він не шифрує дані та не захищає від XSS.
Пароль не потрібно зберігати в localStorage, sessionStorage, cookies або IndexedDB. Сервер має прийняти пароль через захищене з’єднання, а зберігати — лише безпечний хеш за відповідною політикою автентифікації.
Cookies надсилаються разом із відповідними запитами, тому великі значення збільшують мережевий трафік. Для великих локальних даних краще підходить IndexedDB.
SecureУ робочому HTTPS-застосунку сесійна cookie без Secure може бути передана через незахищене HTTP-з’єднання за відповідних умов.
DomainCookie, доступна всім піддоменам, має більшу область ризику. Не вказуйте Domain, якщо спільний доступ між піддоменами не потрібен.
Значення зі сховища не є доказом автентифікації або права доступу. Наприклад, localStorage.setItem("isAdmin", "true") не повинен давати користувачу адміністративні права.
innerHTML для даних зі сховищаДані могли бути змінені вручну або потрапити у сховище через інше джерело. Для тексту використовуйте textContent, а HTML обробляйте спеціальними безпечними правилами.
JSON.parseПошкоджені або застарілі дані можуть викликати виняток і зламати логіку застосунку. Обробляйте помилки та майте стратегію скидання старого формату.
Перед збереженням поставте собі такі запитання:
Чи є дані секретними?
Чи потрібно надсилати їх серверу автоматично?
Чи повинні вони переживати перезапуск браузера?
Чи будуть дані великими або структурованими?
Чи потрібен доступ із кількох вкладок?
Який термін життя цих даних?
Що станеться, якщо користувач змінить або видалить їх?
Загальна стратегія:
несекретні прості налаштування на тривалий час — localStorage;
несекретний тимчасовий стан вкладки — sessionStorage;
сесійна автентифікація, яку має отримувати сервер — HttpOnly cookie;
великі структуровані офлайн-дані — IndexedDB;
секрети, доступні JavaScript-коду, — лише коли це обґрунтовано архітектурою та з мінімальним часом життя.
localStorage зберігає невеликі рядкові значення між сесіями браузера.
sessionStorage призначений для тимчасових даних конкретної вкладки.
Cookies можуть автоматично надсилатися серверу та підтримують атрибути HttpOnly, Secure і SameSite.
IndexedDB підходить для великих структурованих даних і офлайн-режиму.
Дані, доступні JavaScript, не захищені від XSS.
Сесійні cookies зазвичай мають бути HttpOnly, Secure і налаштовані з відповідним SameSite.
Клієнтським сховищам не можна довіряти під час перевірки прав доступу.
Сховище потрібно вибирати за чутливістю даних, терміном життя, розміром і необхідністю передавання на сервер.