Защита приложений Next.js от CSRF-атак

Защита приложений Next.js от CSRF-атак

В этой статье мы рассмотрим атаки Cross-Site Request Forgery (CSRF) в контексте приложения Next.js и способы защиты от них. Сначала мы рассмотрим концепцию CSRF-атак и то, как они могут повлиять на веб-приложение в целом. Для этого мы опишем сценарий, в котором мы запустим CSRF-атаку на наше приложение Next.js. Затем мы используем пакет next-csrf и определенные теги безопасности cookie, чтобы показать, как защититься от этих атак. Исходный код этой заметки можно найти здесь.

Что такое CSRF-атака?

Представьте, что вы вошли на сайт онлайн-банкинга, который устанавливает cookie в вашем браузере для поддержания вашей личной сессии. Этот файл cookie содержит маркер аутентификации, который используется для идентификации вашей сессии и аутентификации ваших запросов.

Очень упрощенный, но возможный HTTP-запрос для отправки денег может быть следующим:

POST /transfer HTTP/1.1
Host: vulnerable-bank.com
Content-Type: application/json
Content-Length: 30
Cookie: session=454544

amount=1000$
name=friendlyuser@gmail.com
iban=DE7823778237873

В то же время при CSRF-атаке у вас в браузере открыта еще одна вкладка, на которой загружен вредоносный сайт. Этот вредоносный сайт может содержать скрытую форму или код JavaScript, который отправляет запрос на сайт онлайн-банка, используя наш маркер аутентификации.

Поскольку запрос инициируется из одного и того же браузера, веб-приложение не может отличить законный запрос, инициированный нами, от поддельного запроса, отправленного злоумышленником. Веб-приложение обработает запрос и выполнит непредусмотренное действие без нашего ведома или согласия.

Как защититься от атаки CSRF

В этом разделе мы обсудим несколько различных способов защиты от CSRF-атак.

Возможный способ защитить ваше приложение Next.js от CSRF-атак - это определить значение SameSite внутри файлов cookie, которые вы используете на своем сайте. Google ввел это значение в 2006 году с целью предотвратить автоматическую отправку файлов cookie вместе с межсайтовыми запросами браузера, как это происходило ранее, что позволило бы минимизировать риск потери конфиденциальной информации и обеспечить защиту от подделки межсайтовых запросов.

Атрибут SameSite может принимать значение strict или lax. В строгом режиме защищенный файл cookie не отправляется ни с одним межсайтовым запросом. Это уже применимо к щелчку на простой ссылке, но если применить это к нашему примеру с онлайн-банкингом, то это означает, что вам придется заново проходить аутентификацию каждый раз, когда вы будете перенаправлены на страницу онлайн-банкинга.

Это не соответствует обычному поведению веб-приложений, поскольку пользователи не хотят постоянно заново входить в систему. К счастью, режим lax несколько смягчает такое поведение и позволяет отправлять cookie вместе с некоторыми ”безопасными” межсайтовыми запросами. Это влияет только на безопасные методы HTTP, доступные только для чтения, и навигацию верхнего уровня (действия, которые приводят к изменению URL в адресной строке браузера, например, ссылки).

Ниже приведен обзор различных типов запросов и их различных вариантов, которые влияют на то, будет ли отправлен файл cookie или нет. Большой палец вверх означает, что cookie будет отправлен. Например, вы можете видеть, что в строгом режиме cookie никогда не будет отправлен вместе с межсайтовым запросом.

Тип запросаПримерБез SameSiteнестрогий режимстрогий режим
a-tag<a href=”..”>👍👍👎
form (get)<form method=”get”…>👍👍👎
form (post)<form method=”post”…>👍👎👎
iframe<iframe src=”..”>👍👎👎
ajax$.get(“…”)👍👎👎
image-tag<img src=”…”>👍👎👎

Установив флаг HttpOnly cookie, вы можете снизить вероятность CSRF-атаки, поскольку HTTP-only cookie не могут быть получены JavaScript через сценарии на стороне клиента.

res.setHeader("Set-Cookie", `session=${sessionId}; Path=/;   Max-Age=600; SameSite=Strict; HttpOnly`);

Использование CSRF-токенов

Одним из способов защиты вашего веб-приложения от CSRF-атаки является использование так называемых CSRF-токенов. CSRF-токен - это уникальное случайное значение, которое генерируется на стороне сервера и включается в каждый запрос, отправляемый клиентом. Если маркер, отправленный клиентом, совпадает с маркером, хранящимся на стороне сервера, запрос считается легитимным и обрабатывается сервером. В противном случае запрос будет отклонен.

Важно отметить, что CSRF-токены обеспечивают эффективную защиту от CSRF-атак до тех пор, пока токен генерируется случайным образом и не может быть легко угадан или предсказан. Кроме того, срок действия токена должен истекать через определенный период времени или после однократного использования, чтобы предотвратить повторное использование злоумышленниками старых токенов.

Как проводить CSRF-атаки на незащищенных веб-страницах

В этом разделе мы рассмотрим код примера страницы онлайн-банкинга и то, как она уязвима для CSRF-атак. После этого мы реализуем защиту от CSRF с помощью пакета next-csrf и установки значения SameSite в куки сессии.

Наш демонстрационный онлайн-банк состоит из двух основных маршрутов: маршрут входа и маршрут перевода. Маршрут перевода доступен только после успешной аутентификации через маршрут входа. Для этого я создал простой API-маршрут для обработки запроса на вход:

// pages/api/login.js

export default function login(req, res) {
  // check the user's credentials
  const { username, password } = req.body;
  let authenticated;

  if (username === "test" && password === "123456") {
    authenticated === true 
  } else {
    authenticated === false
  }
     

  if (authenticated) {
    // set a cookie with the a random sessionId
    const sessionId = 454544;
    res.setHeader("Set-Cookie", `session=${sessionId}; Path=/;   Max-Age=600`);

    // send a success response
    res.status(200).json({ message: "Login successful" });
  } else {
    // send an error response
    res.status(401).json({ message: "Invalid credentials" });
  }
}
The login page

Страница входа в систему выглядит следующим образом:

Самой важной строкой кода в приведенном выше коде, вероятно, является:

res.setHeader("Set-Cookie", `session=${sessionId}; Path=/; Max-Age=600`);

Это устанавливает cookie с идентификатором сессии и продолжительностью 10 минут. Для простоты мы используем жестко закодированные идентификаторы сеанса, имена пользователей и пароли.

После успешной аутентификации вы должны увидеть страницу перевода средств на нашем демонстрационном сайте онлайн-банкинга:

Соответствующий упрощенный API-маршрут для обработки банковских переводов выглядит следующим образом:

// pages/api/transfer.js

export default function handler(req, res) {
  // Check that the request method is POST
  if (req.method !== 'POST') {
    res.status(405).json({ error: 'Method Not Allowed' });
    return;
  }

  // Check that the request has a valid session cookie
  if (!req.cookies.session) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }

  // Parse the JSON data from the request body
  const { amount, name, iban } = req.body;

  // TODO: Implement transfer logic

  // Return a success message
  res.status(200).json({ message: 'Transfer successful' });
}

В приведенном выше коде мы выполняем две проверки: Одна для метода запроса, а другая проверяет сессионный файл cookie.

Мы не собираемся создавать вредоносный сайт; вместо этого мы будем имитировать отправку данных через форму или код JavaScript на этом вредоносном сайте. Поскольку запрос инициируется из того же браузера, к нему автоматически будет прикреплен сеансовый cookie, и наш бэкенд не сможет отличить легитимный запрос, инициированный нами как аутентифицированным пользователем, от поддельного запроса, отправленного злоумышленником.

Все, что нам нужно отправить через этот CURL-запрос, это данные формы (сумма, имя, iban) и сессионный cookie. Соответствующий запрос выглядит следующим образом:

curl -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Cookie: session=1234" \
  -d "iban=1736123125&amount=10000000&name=Criminal" \
  http://localhost:3000/api/transfer

Незащищенный маршрут api/transfer приведет к такому ответу:

{"name":"Criminal","iban":"1736123125","amount":"10000000"}

Этот ответ означает, что мы только что успешно выполнили CSRF-атаку на странице онлайн-банкинга.

Как защитить приложение Next.js от CSRF-атак


Использование маркеров SameSite и HttpOnly

Давайте сначала реализуем атрибуты SameSite и HttpOnly нашего сессионного cookie, поскольку это легко сделать за один шаг. Помните, что мы установили cookie в нашем маршруте API входа в систему, расположенном в src/pages/api/login.js. Давайте настроим параметр cookie в соответствующем маршруте:

res.setHeader("Set-Cookie", `session=${sessionId}; Path=/;   Max-Age=600; SameSite=Strict; HttpOnly`);

Это все, что вам нужно сделать, чтобы настроить сессионный файл cookie. Выбор строгой или мягкой политики зависит от того, насколько высоки ваши требования к безопасности и чем вы готовы пожертвовать в плане пользовательского опыта.

Использование CSRF-токенов

Как уже упоминалось в разделах выше, существует пакет next-csrf, который позволяет легко реализовать следующие шаги для обеспечения защиты от CSRF-атак:

  • Сервер генерирует и отправляет клиенту токен csrf
  • Клиент/браузер отправляет форму с токеном
  • Сервер проверяет, действителен ли токен или нет

Для успешного проведения атаки CSRF злоумышленнику необходимо получить маркер CSRF с вашего сайта и использовать JavaScript для доступа к нему. Это означает, что если на вашем сайте не разрешен кросс-оригинальный обмен ресурсами (CORS), злоумышленник не сможет получить доступ к маркеру CSRF, что эффективно нейтрализует угрозу.

Чтобы установить пакет next-csrf, выполните следующую команду в корне вашего проекта Next.js:

npm i next-csrf --save

На первом этапе давайте инициализируем next-csrf, создав установочный файл. Это создаст промежуточное программное обеспечение для создания и проверки CSRF-токенов:

// "lib/csrf"
import { nextCsrf } from "next-csrf";

const { csrf, setup } = nextCsrf({
 // eslint-disable-next-line no-undef
 secret: "12345",
});

export { csrf, setup };

В производственной среде вы, конечно же, будете хранить свой секрет в файле окружения.

Чтобы установить CSRF-токен, мы будем использовать страницу с рендерингом на стороне сервера, как наша страница входа в систему, потому что вы используете смягчение CSRF для защиты ваших запросов от аутентифицированных пользователей.

import Head from "next/head";
import { setup } from "lib/csrf";

export default function Home() {
 return (
   ...
 );
}

export const getServerSideProps = setup(async ({ req, res }) => {
 return {
   props: {},
 };
});

После этого единственное, что нам нужно сделать для защиты маршрута API, — это обернуть соответствующий маршрут API нашим промежуточным программным обеспечением csrf:

// src/pages/api/transfer.js
import { csrf } from "../../../lib/csrf";

const handler = (req, res) => {
   // Check that the request method is POST
   if (req.method !== 'POST') {
     res.status(405).json({ error: 'Method Not Allowed' });
     return;
   }
    // Check that the request has a valid session cookie
   if (!req.cookies.session) {
     res.status(401).json({ error: 'Unauthorized' });
     return;
   }
    // Parse the JSON data from the request body
   const { name, iban, amount } = req.body;
   console.log(name, iban, amount)
   console.log(req.cookies.session);
    // Return a success message
   res.status(200).json({ name, iban, amount });
 }

 export default csrf(handler);

Перед выполнением логики запроса промежуточное ПО csrf выполнит проверку CSRF-токенов и в случае неудачной проверки выдаст ошибку:

{"message":"Invalid CSRF token"}

Заключение

В этой статье мы рассмотрели тему защиты вашего приложения Next.js от CSRF-атак и подробно рассмотрели пакет next-csrf, который позволяет реализовать защиту от CSRF с помощью CSRF-токенов. Кроме того, мы рассмотрели конфигурацию cookie-файлов и способы повышения безопасности путем установки определенных значений cookie-файлов.