\n\n4. Браузер автоматически отправляет POST запрос на bank.com\n вместе с cookies (потому что пользователь авторизован)\n\n5. bank.com получает запрос и думает, что это легитимный запрос\n от авторизованного пользователя (потому что есть cookies)\n\n6. Деньги переводятся на счёт злоумышленника!\n```\n\n### Почему это работает\n\nБраузер автоматически отправляет cookies для запросов на тот же домен, где они были установлены. evil.com не может прочитать cookies (из-за Same-Origin Policy), но может заставить браузер отправить запрос с ними.\n\n## Защита 1: CSRF Tokens\n\nЭто основной способ защиты. Идея:\n- Для каждой критической операции сервер генерирует уникальный токен\n- Клиент должен отправить этот токен в запросе\n- evil.com не может узнать токен (он не известен)\n\n### Использование csurf middleware\n\n```bash\nnpm install csurf\n```\n\n```javascript\nconst express = require(\"express\");\nconst csrf = require(\"csurf\");\nconst session = require(\"express-session\");\nconst cookieParser = require(\"cookie-parser\");\n\nconst app = express();\n\n// 1. Парсирование cookies и session\napp.use(cookieParser());\napp.use(session({\n secret: \"your-secret-key\",\n resave: false,\n saveUninitialized: true,\n cookie: {\n httpOnly: true,\n secure: true, // Только HTTPS\n sameSite: \"strict\",\n maxAge: 3600000 // 1 час\n }\n}));\n\n// 2. Парсирование body\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// 3. CSRF middleware (хранит токен в session)\nconst csrfProtection = csrf({ cookie: false }); // используем session, не cookie\n\n// 4. GET запросы - возвращаем форму с CSRF токеном\napp.get(\"/transfer\", csrfProtection, (req, res) => {\n res.send(`\n
\n \n \n \n \n
\n `);\n});\n\n// 5. POST запросы - проверяем CSRF токен\napp.post(\"/transfer\", csrfProtection, (req, res) => {\n const { to, amount } = req.body;\n \n // Если токен неверный, csurf выбросит ошибку\n console.log(`Transferring ${amount} to ${to}`);\n \n res.json({ success: true, message: \"Transfer completed\" });\n});\n\n// 6. Обработчик ошибок CSRF\napp.use((err, req, res, next) => {\n if (err.code !== \"EBADCSRFTOKEN\") return next(err);\n \n res.status(403).json({ error: \"Invalid CSRF token\" });\n});\n\napp.listen(3000);\n```\n\n## CSRF Token для SPA (JSON API)\n\nДля приложений, работающих с JSON (не HTML формы):\n\n```javascript\n// Middleware для предоставления CSRF токена\napp.get(\"/api/csrf-token\", csrfProtection, (req, res) => {\n res.json({ csrfToken: req.csrfToken() });\n});\n\n// POST с CSRF защитой\napp.post(\"/api/transfer\", csrfProtection, (req, res) => {\n const { to, amount } = req.body;\n console.log(`Transfer: ${amount} to ${to}`);\n res.json({ success: true });\n});\n```\n\nНа фронтенде (React, Vue, Angular):\n\n```javascript\n// 1. Получить токен при загрузке приложения\nconst getCsrfToken = async () => {\n const response = await fetch(\"/api/csrf-token\");\n const { csrfToken } = await response.json();\n return csrfToken;\n};\n\n// 2. Отправить запрос с токеном\nconst transfer = async (to, amount) => {\n const csrfToken = await getCsrfToken();\n \n const response = await fetch(\"/api/transfer\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-CSRF-Token\": csrfToken // Отправляем токен в заголовке\n },\n body: JSON.stringify({ to, amount })\n });\n \n return response.json();\n};\n```\n\n## Защита 2: SameSite Cookie Attribute\n\nСовременный способ - флаг SameSite:\n\n```javascript\napp.use(session({\n secret: \"secret\",\n cookie: {\n sameSite: \"strict\", // Запретить отправку cookies для cross-site запросов\n httpOnly: true,\n secure: true\n }\n}));\n```\n\nЗначения:\n- `\"strict\"` - cookies НЕ отправляются в cross-site запросах вообще\n- `\"lax\"` - cookies отправляются только для GET запросов (навигация, img src)\n- `\"none\"` - cookies отправляются всегда (требует `secure: true`)\n\nЭто защищает от большинства CSRF атак автоматически.\n\n```javascript\n// Пример: evil.com больше не может отправить POST с cookies\n
\n \n
\n```\n\n## Защита 3: Origin и Referer проверка\n\n```javascript\nconst checkOrigin = (req, res, next) => {\n const origin = req.get(\"origin\");\n const referer = req.get(\"referer\");\n const allowedOrigins = [\"https://myapp.com\"];\n \n if (origin && !allowedOrigins.includes(origin)) {\n return res.status(403).json({ error: \"CSRF: Invalid origin\" });\n }\n \n if (referer && !referer.startsWith(\"https://myapp.com\")) {\n return res.status(403).json({ error: \"CSRF: Invalid referer\" });\n }\n \n next();\n};\n\napp.post(\"/api/sensitive-action\", checkOrigin, (req, res) => {\n // Обработка запроса\n res.json({ success: true });\n});\n```\n\n## Полный защищённый пример\n\n```javascript\nconst express = require(\"express\");\nconst csrf = require(\"csurf\");\nconst session = require(\"express-session\");\nconst cookieParser = require(\"cookie-parser\");\nconst helmet = require(\"helmet\");\n\nconst app = express();\n\n// Безопасность заголовков\napp.use(helmet());\n\n// Парсирование\napp.use(cookieParser());\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Session\napp.use(session({\n secret: process.env.SESSION_SECRET,\n resave: false,\n saveUninitialized: false,\n cookie: {\n httpOnly: true,\n secure: process.env.NODE_ENV === \"production\",\n sameSite: \"strict\",\n maxAge: 3600000\n }\n}));\n\n// CSRF защита\nconst csrfProtection = csrf({ cookie: false });\n\n// Проверка origin для POST/PUT/DELETE\nconst checkOrigin = (req, res, next) => {\n if ([\"POST\", \"PUT\", \"DELETE\", \"PATCH\"].includes(req.method)) {\n const origin = req.get(\"origin\");\n if (origin && !origin.startsWith(\"https://myapp.com\")) {\n return res.status(403).json({ error: \"CSRF blocked\" });\n }\n }\n next();\n};\n\napp.use(checkOrigin);\n\n// Публичные эндпоинты\napp.get(\"/\", (req, res) => {\n res.send(\"

Banking App

\");\n});\n\n// API для получения CSRF токена\napp.get(\"/api/csrf-token\", csrfProtection, (req, res) => {\n res.json({ csrfToken: req.csrfToken() });\n});\n\n// HTML форма с CSRF\napp.get(\"/transfer\", csrfProtection, (req, res) => {\n res.send(`\n
\n \n \n \n \n
\n `);\n});\n\n// POST с CSRF защитой\napp.post(\"/transfer\", csrfProtection, (req, res) => {\n const { to, amount } = req.body;\n // Обработка трансфера\n res.json({ success: true, transferred: amount, to });\n});\n\n// Обработчик CSRF ошибок\napp.use((err, req, res, next) => {\n if (err.code === \"EBADCSRFTOKEN\") {\n return res.status(403).json({ error: \"CSRF token validation failed\" });\n }\n next(err);\n});\n\napp.listen(3000, () => {\n console.log(\"Bank app running on https://localhost:3000\");\n});\n```\n\n## Тестирование CSRF защиты\n\n```bash\n# 1. Попытка без токена (должна не пройти)\ncurl -X POST http://localhost:3000/transfer \\\n -H \"Content-Type: application/json\" \\\n -d '{\"to\": \"attacker\", \"amount\": 1000}'\n# Ответ: 403 Forbidden\n\n# 2. Попытка с неверным токеном\ncurl -X POST http://localhost:3000/transfer \\\n -H \"Content-Type: application/json\" \\\n -H \"X-CSRF-Token: invalid-token\" \\\n -d '{\"to\": \"attacker\", \"amount\": 1000}'\n# Ответ: 403 Forbidden\n```\n\n## Best Practices\n\n- **Всегда используй CSRF защиту** для POST/PUT/DELETE операций\n- **SameSite cookies** - это first line of defense в современных браузерах\n- **CSRF токены** - для дополнительной защиты и старых браузеров\n- **Origin/Referer проверка** - для defence in depth\n- **HTTPS обязателен** - CSRF защита требует secure cookies\n- **Ротируй токены** - генерируй новый после каждого использования (опционально)\n- **Краткосрочные токены** - токены должны иметь срок действия\n\nCSRF атаки опасны потому что используют доверие между браузером и сервером. Используй многоуровневую защиту.","dateCreated":"2026-03-30T13:45:56.676157","upvoteCount":0,"author":{"@type":"Person","name":"claude-haiku-4.5"}}}}
← Назад к вопросам

Что такое CSRF атака и как защититься в Express приложении?

2.0 Middle🔥 181 комментариев
#Node.js и JavaScript#Безопасность#Фреймворки и библиотеки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

CSRF атаки и защита в Express

CSRF (Cross-Site Request Forgery) - это атака, при которой злоумышленник заставляет пользователя выполнить нежелательное действие на сайте, где он авторизован. Это одна из топ-10 уязвимостей по OWASP.

Как работает CSRF атака

Сценарий атаки

1. Пользователь авторизуется на bank.com
   GET /login -> браузер сохраняет cookies с sessionId

2. Пользователь (не выходя) открывает악意ful.com
   (например, по ссылке в письме)

3. На evil.com находится код:
   <form action="https://bank.com/transfer" method="POST">
     <input type="hidden" name="to" value="attacker">
     <input type="hidden" name="amount" value="1000">
   </form>
   <script>document.forms[0].submit();</script>

4. Браузер автоматически отправляет POST запрос на bank.com
   вместе с cookies (потому что пользователь авторизован)

5. bank.com получает запрос и думает, что это легитимный запрос
   от авторизованного пользователя (потому что есть cookies)

6. Деньги переводятся на счёт злоумышленника!

Почему это работает

Браузер автоматически отправляет cookies для запросов на тот же домен, где они были установлены. evil.com не может прочитать cookies (из-за Same-Origin Policy), но может заставить браузер отправить запрос с ними.

Защита 1: CSRF Tokens

Это основной способ защиты. Идея:

  • Для каждой критической операции сервер генерирует уникальный токен
  • Клиент должен отправить этот токен в запросе
  • evil.com не может узнать токен (он не известен)

Использование csurf middleware

npm install csurf
const express = require("express");
const csrf = require("csurf");
const session = require("express-session");
const cookieParser = require("cookie-parser");

const app = express();

// 1. Парсирование cookies и session
app.use(cookieParser());
app.use(session({
  secret: "your-secret-key",
  resave: false,
  saveUninitialized: true,
  cookie: {
    httpOnly: true,
    secure: true,      // Только HTTPS
    sameSite: "strict",
    maxAge: 3600000    // 1 час
  }
}));

// 2. Парсирование body
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// 3. CSRF middleware (хранит токен в session)
const csrfProtection = csrf({ cookie: false });  // используем session, не cookie

// 4. GET запросы - возвращаем форму с CSRF токеном
app.get("/transfer", csrfProtection, (req, res) => {
  res.send(`
    <form action="/transfer" method="POST">
      <input type="hidden" name="_csrf" value="${req.csrfToken()}">
      <label>
        To Account:
        <input type="text" name="to" required>
      </label>
      <label>
        Amount:
        <input type="number" name="amount" required>
      </label>
      <button type="submit">Transfer</button>
    </form>
  `);
});

// 5. POST запросы - проверяем CSRF токен
app.post("/transfer", csrfProtection, (req, res) => {
  const { to, amount } = req.body;
  
  // Если токен неверный, csurf выбросит ошибку
  console.log(`Transferring ${amount} to ${to}`);
  
  res.json({ success: true, message: "Transfer completed" });
});

// 6. Обработчик ошибок CSRF
app.use((err, req, res, next) => {
  if (err.code !== "EBADCSRFTOKEN") return next(err);
  
  res.status(403).json({ error: "Invalid CSRF token" });
});

app.listen(3000);

CSRF Token для SPA (JSON API)

Для приложений, работающих с JSON (не HTML формы):

// Middleware для предоставления CSRF токена
app.get("/api/csrf-token", csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// POST с CSRF защитой
app.post("/api/transfer", csrfProtection, (req, res) => {
  const { to, amount } = req.body;
  console.log(`Transfer: ${amount} to ${to}`);
  res.json({ success: true });
});

На фронтенде (React, Vue, Angular):

// 1. Получить токен при загрузке приложения
const getCsrfToken = async () => {
  const response = await fetch("/api/csrf-token");
  const { csrfToken } = await response.json();
  return csrfToken;
};

// 2. Отправить запрос с токеном
const transfer = async (to, amount) => {
  const csrfToken = await getCsrfToken();
  
  const response = await fetch("/api/transfer", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": csrfToken  // Отправляем токен в заголовке
    },
    body: JSON.stringify({ to, amount })
  });
  
  return response.json();
};

Защита 2: SameSite Cookie Attribute

Современный способ - флаг SameSite:

app.use(session({
  secret: "secret",
  cookie: {
    sameSite: "strict",  // Запретить отправку cookies для cross-site запросов
    httpOnly: true,
    secure: true
  }
}));

Значения:

  • "strict" - cookies НЕ отправляются в cross-site запросах вообще
  • "lax" - cookies отправляются только для GET запросов (навигация, img src)
  • "none" - cookies отправляются всегда (требует secure: true)

Это защищает от большинства CSRF атак автоматически.

// Пример: evil.com больше не может отправить POST с cookies
<form action="https://bank.com/transfer" method="POST">
  <!-- Браузер НЕ отправит sessionId cookies! -->
</form>

Защита 3: Origin и Referer проверка

const checkOrigin = (req, res, next) => {
  const origin = req.get("origin");
  const referer = req.get("referer");
  const allowedOrigins = ["https://myapp.com"];
  
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: "CSRF: Invalid origin" });
  }
  
  if (referer && !referer.startsWith("https://myapp.com")) {
    return res.status(403).json({ error: "CSRF: Invalid referer" });
  }
  
  next();
};

app.post("/api/sensitive-action", checkOrigin, (req, res) => {
  // Обработка запроса
  res.json({ success: true });
});

Полный защищённый пример

const express = require("express");
const csrf = require("csurf");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const helmet = require("helmet");

const app = express();

// Безопасность заголовков
app.use(helmet());

// Парсирование
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// Session
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 3600000
  }
}));

// CSRF защита
const csrfProtection = csrf({ cookie: false });

// Проверка origin для POST/PUT/DELETE
const checkOrigin = (req, res, next) => {
  if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method)) {
    const origin = req.get("origin");
    if (origin && !origin.startsWith("https://myapp.com")) {
      return res.status(403).json({ error: "CSRF blocked" });
    }
  }
  next();
};

app.use(checkOrigin);

// Публичные эндпоинты
app.get("/", (req, res) => {
  res.send("<h1>Banking App</h1>");
});

// API для получения CSRF токена
app.get("/api/csrf-token", csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// HTML форма с CSRF
app.get("/transfer", csrfProtection, (req, res) => {
  res.send(`
    <form action="/transfer" method="POST">
      <input type="hidden" name="_csrf" value="${req.csrfToken()}">
      <input type="text" name="to" placeholder="Account number">
      <input type="number" name="amount" placeholder="Amount">
      <button>Transfer</button>
    </form>
  `);
});

// POST с CSRF защитой
app.post("/transfer", csrfProtection, (req, res) => {
  const { to, amount } = req.body;
  // Обработка трансфера
  res.json({ success: true, transferred: amount, to });
});

// Обработчик CSRF ошибок
app.use((err, req, res, next) => {
  if (err.code === "EBADCSRFTOKEN") {
    return res.status(403).json({ error: "CSRF token validation failed" });
  }
  next(err);
});

app.listen(3000, () => {
  console.log("Bank app running on https://localhost:3000");
});

Тестирование CSRF защиты

# 1. Попытка без токена (должна не пройти)
curl -X POST http://localhost:3000/transfer \
  -H "Content-Type: application/json" \
  -d '{"to": "attacker", "amount": 1000}'
# Ответ: 403 Forbidden

# 2. Попытка с неверным токеном
curl -X POST http://localhost:3000/transfer \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: invalid-token" \
  -d '{"to": "attacker", "amount": 1000}'
# Ответ: 403 Forbidden

Best Practices

  • Всегда используй CSRF защиту для POST/PUT/DELETE операций
  • SameSite cookies - это first line of defense в современных браузерах
  • CSRF токены - для дополнительной защиты и старых браузеров
  • Origin/Referer проверка - для defence in depth
  • HTTPS обязателен - CSRF защита требует secure cookies
  • Ротируй токены - генерируй новый после каждого использования (опционально)
  • Краткосрочные токены - токены должны иметь срок действия

CSRF атаки опасны потому что используют доверие между браузером и сервером. Используй многоуровневую защиту.

Что такое CSRF атака и как защититься в Express приложении? | PrepBro