Пошук уроків, статей та іншого контенту
Розберете модель загроз вебзастосунку, активи, точки входу та типові атаки на код JavaScript у браузері.
Модель загроз — це структурований спосіб відповісти на запитання:
що ми захищаємо;
від кого захищаємо;
через які точки застосунок може бути атакований;
що станеться у разі успішної атаки;
які заходи зменшують ризик.
Модель загроз не означає, що застосунок обов’язково буде атаковано. Вона допомагає заздалегідь знайти небезпечні місця та не покладатися на випадковість.
Для браузерного застосунку важливо пам’ятати:
Код JavaScript, HTML і CSS завантажується на пристрій користувача. Користувач може переглядати, змінювати та запускати цей код як завгодно.
Тому браузерний код не може містити секрети, які потрібно приховати від користувача.
Типовий вебзастосунок складається з кількох частин:
Браузерний клієнт
HTML;
CSS;
JavaScript;
дані у localStorage, sessionStorage, cookies;
DOM-елементи сторінки.
Сервер
автентифікація;
перевірка прав доступу;
база даних;
бізнес-логіка.
Зовнішні сервіси
платіжні системи;
аналітика;
карти;
віджети та сторонні скрипти.
У кожної частини своя зона відповідальності. Наприклад, JavaScript у браузері може приховати кнопку від користувача, але це не є перевіркою доступу. Користувач може вручну викликати API або змінити код сторінки.
Правильна перевірка прав має виконуватися на сервері.
Активи — це дані або ресурси, які потрібно захистити.
Приклади активів:
пароль користувача;
токен сесії;
персональні дані;
баланс рахунку;
замовлення;
права адміністратора;
дані бази даних;
доступ до API;
цілісність HTML-сторінки.
Для кожного активу потрібно визначити:
хто може його читати;
хто може його змінювати;
що буде, якщо він стане доступним зловмиснику;
чи можна відновити його після пошкодження.
Точка входу — це місце, через яке дані або команди потрапляють у застосунок.
У браузерному JavaScript точками входу можуть бути:
поля форм;
параметри URL;
фрагмент URL після #;
дані з localStorage;
відповіді API;
cookies;
повідомлення через postMessage;
файли, вибрані користувачем;
дані з WebSocket;
сторонні скрипти;
HTML, доданий у DOM.
Наприклад, значення з адреси:
https://example.com/search?q=javascriptє зовнішніми даними. Навіть якщо URL сформував ваш застосунок, він усе одно може бути змінений користувачем.
Зловмисник — це не обов’язково спеціаліст, який має доступ до сервера. Для браузерного застосунку варто враховувати кілька сценаріїв:
користувач змінює JavaScript у DevTools;
користувач передає спеціально підготовлений текст у форму;
зловмисник надсилає жертві спеціальне посилання;
шкідливий сайт намагається взаємодіяти з вашим застосунком;
скомпрометований сторонній скрипт виконується на вашій сторінці;
атакер отримав токен сесії або доступ до облікового запису.
Найважливіше правило браузерної безпеки:
Дані, які надходять із браузера, потрібно вважати недовіреними.
Це стосується:
введення у форми;
URL;
значень із localStorage;
даних із DOM;
відповідей зовнішніх сервісів;
значень, які нібито згенерував ваш JavaScript.
Навіть якщо дані перевіряються в браузері, користувач може:
вимкнути цю перевірку;
змінити JavaScript;
відправити запит без браузера;
викликати API власним скриптом.
Перевірка у браузері корисна для зручності користувача, але не замінює перевірку на сервері.
XSS, або Cross-Site Scripting, — це виконання небажаного JavaScript-коду в контексті вашої сторінки.
Зловмисник може спробувати додати на сторінку код, який:
змінює вміст сторінки;
показує фальшиву форму входу;
виконує дії від імені користувача;
читає доступні JavaScript дані;
викрадає інформацію з DOM.
innerHTMLРозглянемо код:
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Небезпечний приклад</title>
</head>
<body>
<input id="name" placeholder="Введіть ім'я">
<button id="show">Показати</button>
<div id="message"></div>
<script>
const nameInput = document.querySelector("#name");
const message = document.querySelector("#message");
document.querySelector("#show").addEventListener("click", () => {
// Небезпечно: введення користувача інтерпретується як HTML
message.innerHTML = `Вітаємо, ${nameInput.value}!`;
});
</script>
</body>
</html>Якщо користувач введе звичайний текст, результат виглядатиме нормально. Але innerHTML розглядає значення як HTML, а не як простий текст.
Для відображення тексту використовуйте textContent:
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Безпечне відображення тексту</title>
</head>
<body>
<input id="name" placeholder="Введіть ім'я">
<button id="show">Показати</button>
<div id="message"></div>
<script>
const nameInput = document.querySelector("#name");
const message = document.querySelector("#message");
document.querySelector("#show").addEventListener("click", () => {
// Безпечно: введення додається як звичайний текст
message.textContent = `Вітаємо, ${nameInput.value}!`;
});
</script>
</body>
</html>textContent не виконує HTML, що міститься у значенні.
innerHTML може бути допустимимІноді застосунку потрібно створювати HTML-розмітку. У такому разі не можна безпосередньо вставляти в innerHTML недовірені дані.
Краще:
створювати елементи через document.createElement;
додавати текст через textContent;
дозволяти лише наперед визначені значення;
використовувати перевірену бібліотеку санітизації, якщо справді потрібно підтримувати HTML від користувача.
Наприклад:
const item = document.createElement("li");
item.textContent = userProvidedText;
list.append(item);DOM-based XSS виникає, коли небезпечне значення з браузера потрапляє у небезпечний DOM-контекст.
Потенційно небезпечними джерелами є:
location.search;
location.hash;
document.referrer;
localStorage;
postMessage.
Потенційно небезпечними приймачами є:
innerHTML;
outerHTML;
insertAdjacentHTML;
document.write;
деякі способи створення URL;
виконання рядка як коду.
Простий безпечний приклад читання параметра URL:
const params = new URLSearchParams(location.search);
const query = params.get("q") ?? "";
document.querySelector("#search-query").textContent = query;Тут значення показується як текст, а не виконується як розмітка.
localStoragelocalStorage зручний для збереження налаштувань, наприклад теми або мови:
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme") ?? "light";
document.documentElement.dataset.theme = theme;Але localStorage має важливі обмеження:
його може прочитати будь-який JavaScript-код на тому самому origin;
дані не шифруються автоматично;
вони не захищаються від XSS;
користувач може змінити їх вручну;
не можна довіряти значенням із localStorage.
Не варто зберігати там:
паролі;
довгоживучі токени з високими привілеями;
секретні ключі;
критичні персональні дані.
Якщо шкідливий скрипт виконається на сторінці, він зможе прочитати localStorage.
Cookies часто використовують для зберігання ідентифікатора сесії.
Важливі атрибути cookie:
HttpOnly — забороняє читати cookie через document.cookie;
Secure — надсилає cookie лише через HTTPS;
SameSite — обмежує надсилання cookie у міжсайтових запитах.
HttpOnly особливо корисний проти крадіжки cookie через JavaScript. Однак він не робить XSS нешкідливим: шкідливий код усе ще може виконувати дії від імені користувача, якщо браузер автоматично надсилає cookie на сервер.
Токени в браузері не потрібно вважати секретами від самого користувача. Користувач контролює власний браузер і може переглядати мережеві запити.
CSRF, або Cross-Site Request Forgery, — це атака, під час якої сторонній сайт намагається змусити браузер користувача виконати дію на іншому сайті.
Наприклад:
користувач увійшов до інтернет-банкінгу;
браузер зберігає cookie сесії;
користувач відкриває шкідливий сайт;
шкідливий сайт надсилає запит до банкінгу;
браузер може автоматично додати cookie до цього запиту.
Захист від CSRF зазвичай включає:
cookies з відповідним SameSite;
CSRF-токени;
перевірку заголовків Origin або Referer на сервері;
вимогу спеціального заголовка для небезпечних операцій;
перевірку прав на сервері.
Тільки JavaScript у браузері не може надійно вирішити проблему CSRF.
Same-Origin Policy — правило браузера, яке обмежує взаємодію між різними origin.
Origin визначається трьома частинами:
протоколом;
доменом;
портом.
Наприклад, ці адреси мають різні origin:
https://example.com
http://example.com
https://api.example.com
https://example.com:8443Браузер може заборонити JavaScript читати відповідь від іншого origin. CORS — це механізм, за допомогою якого сервер явно повідомляє браузеру, які cross-origin запити дозволені.
CORS:
не є автентифікацією;
не замінює перевірку прав;
не захищає сервер від запитів, виконаних не з браузера;
визначає, чи може браузерний код прочитати відповідь.
Неправильно вважати, що CORS сам по собі робить API безпечним. Сервер все одно повинен перевіряти автентифікацію, права та дані запиту.
У JavaScript не можна приховати секрет:
const apiKey = "secret-value";Такий ключ можна знайти:
у вихідному коді;
у файлах після збірки;
у DevTools;
у мережевих запитах;
у вихідних map-файлах.
Публічні ключі іноді потрібні frontend-коду, але їхні права мають бути обмежені. Секретні ключі повинні залишатися на сервері.
Такий код не захищає адміністративну функцію:
const isAdmin = localStorage.getItem("isAdmin") === "true";
if (isAdmin) {
document.querySelector("#delete-button").hidden = false;
}Користувач може змінити значення:
localStorage.setItem("isAdmin", "true");Приховування кнопки — це лише елемент інтерфейсу. Сервер має перевіряти роль під час кожної адміністративної операції.
Якщо застосунок без перевірки перенаправляє на адресу з URL, його можуть використати для фішингу:
const next = new URLSearchParams(location.search).get("next");
if (next) {
location.href = next;
}Безпечніше дозволяти лише відомі внутрішні маршрути:
const next = new URLSearchParams(location.search).get("next");
const allowedPaths = new Set(["/", "/profile", "/settings"]);
if (allowedPaths.has(next)) {
location.href = next;
}postMessagepostMessage дозволяє обмінюватися повідомленнями між вікнами. Отримані повідомлення не можна приймати без перевірки відправника:
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.example") {
return;
}
if (event.data?.type === "set-theme") {
document.documentElement.dataset.theme = event.data.theme;
}
});Потрібно перевіряти:
event.origin;
структуру event.data;
допустимі значення;
необхідність самої операції.
Для невеликого застосунку можна використати такий алгоритм.
Наприклад:
користувач входить у профіль;
змінює ім’я;
переглядає замовлення;
адміністратор видаляє товар.
Для цього прикладу активами можуть бути:
обліковий запис;
персональні дані;
замовлення;
права адміністратора;
токен сесії.
Запитайте:
які дані надходять через форми;
які значення читаються з URL;
які відповіді приходять від API;
які дані зберігаються в браузері;
які сторонні скрипти виконуються;
які повідомлення приймаються через postMessage.
Прикладами меж довіри є:
браузер і сервер;
ваш origin і сторонній origin;
frontend і API;
застосунок і сторонній скрипт;
користувацький текст і HTML-шаблон.
На межі довіри дані потрібно перевіряти та правильно обробляти.
Для кожної загрози запитайте:
чи може атакер прочитати дані;
чи може змінити дані;
чи може виконати дію;
чи може обійти перевірку;
чи може зробити застосунок недоступним.
Приклади захисту:
textContent замість небезпечного innerHTML;
перевірка даних на сервері;
перевірка прав на сервері;
cookies з HttpOnly, Secure, SameSite;
HTTPS;
обмеження CORS;
перевірка event.origin;
мінімальні права API;
обмеження сторонніх скриптів;
Content Security Policy.
Content Security Policy, або CSP, — це заголовок HTTP, який обмежує джерела, з яких сторінка може завантажувати та виконувати ресурси.
CSP може зменшити наслідки деяких XSS-атак, наприклад забороняючи inline-скрипти або обмежуючи джерела script.
CSP не замінює:
правильне екранування даних;
перевірку прав;
захист від CSRF;
безпеку серверного API.
Це додатковий рівень захисту, а не єдина лінія оборони.
Нижче наведено невеликий застосунок, який бере пошуковий запит з URL і відображає його без виконання як HTML.
Відкрийте файл і додайте до адреси параметр ?q=JavaScript.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Безпечний пошук</title>
<style>
body {
font-family: sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 0 16px;
}
#result {
padding: 12px;
background: #f1f1f1;
border-radius: 6px;
}
</style>
</head>
<body>
<h1>Пошук</h1>
<form id="search-form">
<label>
Запит:
<input id="query" name="q" autocomplete="off">
</label>
<button type="submit">Знайти</button>
</form>
<p id="result"></p>
<script>
const form = document.querySelector("#search-form");
const queryInput = document.querySelector("#query");
const result = document.querySelector("#result");
const params = new URLSearchParams(location.search);
const initialQuery = params.get("q") ?? "";
// Значення з URL показуємо як текст
queryInput.value = initialQuery;
result.textContent = initialQuery
? `Ви шукаєте: ${initialQuery}`
: "Введіть пошуковий запит";
form.addEventListener("submit", (event) => {
event.preventDefault();
const query = queryInput.value.trim();
// Не вставляємо введення користувача через innerHTML
result.textContent = query
? `Ви шукаєте: ${query}`
: "Пошуковий запит порожній";
const newUrl = new URL(location.href);
if (query) {
newUrl.searchParams.set("q", query);
} else {
newUrl.searchParams.delete("q");
}
history.replaceState(null, "", newUrl);
});
</script>
</body>
</html>У цьому прикладі:
URL вважається недовіреним джерелом;
значення URL читається через URLSearchParams;
текст вставляється через textContent;
введення не виконується як HTML;
URL оновлюється через URL і URLSearchParams.
Користувач може показати приховану кнопку або викликати функцію вручну.
Правильно: перевіряти права на сервері.
Мініфікація та обфускація не перетворюють секрет на недоступний.
Правильно: виконувати секретні операції на сервері.
innerHTML для будь-якого текстуinnerHTML потрібен для HTML, а не для звичайного тексту.
Правильно: використовувати textContent або createElement.
localStorageКористувач може змінити будь-яке значення у localStorage.
Правильно: використовувати його для налаштувань, але не для авторизації чи контролю прав.
Клієнтську перевірку можна обійти.
Правильно: повторювати критичну перевірку на сервері.
CORS визначає, які відповіді може прочитати браузерний код. Він не замінює автентифікацію та авторизацію.
postMessageПовідомлення може надіслати інший сайт.
Правильно: перевіряти event.origin і структуру даних.
Сторонній скрипт виконується з доступом до сторінки. Якщо його скомпрометують, це може вплинути і на ваш застосунок.
Правильно: обмежувати кількість залежностей, використовувати перевірені джерела та контролювати політику завантаження скриптів.
Модель загроз описує активи, зловмисників, точки входу та можливі наслідки атак.
Усі дані з браузера потрібно вважати недовіреними.
JavaScript, HTML і CSS у браузері не є секретними.
textContent безпечніший за innerHTML для відображення тексту.
XSS може виникнути, коли недовірені дані потрапляють у HTML або код.
localStorage не підходить для зберігання секретів і не захищає від XSS.
Приховування кнопки або перевірка ролі у frontend-коді не захищають серверні операції.
Сервер повинен перевіряти автентифікацію, права та вхідні дані.
Cookies, CSRF, CORS, postMessage і сторонні скрипти потрібно враховувати як частини моделі загроз.
Безпека застосунку — це не одна перевірка, а кілька незалежних рівнів захисту.