Схема CSRF-атаки
Схема CSRF-атаки

Ничто так не бесит при изучении новых пакетов/библиотек, как неработающие примеры из официальной документации. До последнего не веришь, что авторы библиотеки так лоханулись с исходниками примеров. Считаешь, что программисты потратили кучу своего времени на разработку, тестирование и продвижение пакета. И что они не могли выложить неработающие примеры. А если примеры не работают, то значит что-то не так у тебя. То ли VPN новый глючит, то ли антивирус душит библиотеку, то ли устаревшие версии какого-то ПО/драйверов/библиотек конфликтуют. В данной статье рассказывается о моем опыте делания рабочим примера npm пакета 'csrf-csrf' из официальной документации.

Кому нужно срочно - вот github с исходниками: https://github.com/korvintaG/csrf-csrf_demo. Важно - обращайте внимание на комментарии, особенно те, в которых много звездочек.

Что делает пакет csrf-csrf?

Предоставляет защиту от CSRF-атак. Судя по отзывам и мнению тех специалистов, которым я доверяю, делает это весьма неплохо. Что такое CSRF-атака можно почитать в т.ч. и на хабре: https://habr.com/ru/articles/318748/

Почему не csurf?

Самый используемый пакет для защиты от CRSF-атак это csurf. Но там не все так просто. В нем нашли уязвимости, автор отказался исправлять, его захэйтили (или достали по простому), и он опубликовал эмоциональное сообщение на главной странице своего пакета:

Главная страница пакета csurf сейчас
Главная страница пакета csurf сейчас

А поскольку мне нужна была CSRF-защита для проектной работы, в рамках сдачи которой была автоматическая проверка пакетов на уязвимости, то csurf был не вариант.

Ошибка №1 - демонстрационный пример тупо не запускается

На главной странице пакета csrf-csrf есть ссылка на github: https://github.com/Psifi-Solutions/csrf-csrf. Скачал репозиторий, увидел примеры, зашел в папку "\example\complete", установил зависимости, запускаю проект, и получаю ошибку сразу же:

Длинный текст ошибки

PS F:\Projects\csrf-csrf\example\complete> npm start
csrf-csrf-complete-example@1.0.0 start
node ./src/index.js
node:internal/modules/esm/resolve:257
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'F:\Projects\csrf-csrf\example\complete\node_modules\csrf-csrf\lib\esm\index
.js' imported from F:\Projects\csrf-csrf\example\complete\src\index.js
at finalizeResolution (node:internal/modules/esm/resolve:257:11)
at moduleResolve (node:internal/modules/esm/resolve:913:10)
at defaultResolve (node:internal/modules/esm/resolve:1037:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:650:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:599:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:582:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:241:38)
at ModuleJob._link (node:internal/modules/esm/module_job:132:49) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///F:/Projects/csrf-csrf/example/complete/node_modules/csrf-csrf/lib/esm/index.js'
}
Node.js v22.11.0
PS F:\Projects\csrf-csrf\example\complete>

Заградительный барьер для джуниоров?

Не, я понимаю, что все эти ошибки для настоящих пацанов, у которых XXX лет опыта в Node.js, есть незаслуживающие внимания мелочи. Я допускаю, что такие ошибки как выше и нижеперечисленные допускаются специально, чтобы синьорам нескучно было новые пакеты мацать. Однако, даже нашему опытному наставнику потребовалось 20 минут времени чтобы заставить демо-пример заработать (за что ему огромное спасибо!). Сколько нужно новичку - не знаю. Мне бы без наставника пришлось минимум неделю напряженного труда потратить, чтобы отловить все косяки.

Исправление ошибки №1

Помните знаменитую цитату: "хотели как лучше, а получилось как всегда". То же и с демо-примерами. Авторы пакета решили, что здорово в демо-примерах добавить зависимость не от стандартного репозитория npm для пакета csrf-csrf, а от ранее собранного по уровню каталогов на один вверх. Также авторы пакета решили, что есть лишь одна ОС - это Linux. Я тоже люблю Linux, но хотя бы предупреждать надо! В общем хитро настроенный package.json как-то конфликтует с symlink от Windows (или еще с чем-то), и в node_modules образуется интересный бесконечный каталог. Нерабочий, кстати. Я как увидел вот такое безобразие:

F:\Projects\csrf-csrf\example\complete\node_modules\csrf-csrf\example\complete\node_modules\csrf-csrf\example\complete\node_modules...

решил, что у меня файловая таблица слетела. А оказалась что выпендреж с package.json. Как исправить? Просто в package.json примера в разделе "dependencies" вместо строки

"csrf-csrf": "file:../..",

прописать строку

"csrf-csrf": "^3.1.0",

Удалить ошибочно сформированную папку "node_modules" и переустановить зависимости. Пакет благополучно запустится.

Для удобства понимания исправлений я правку каждой ошибки оформил в виде отдельного коммита. Начальный коммит я назвал "Init". Исправление этой ошибки в коммите "dependencies".

Ошибка №2 - или запуск примера не значит что он работает

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

"form processed successfully"

А оно не выводится. Но в консоли страницы радостно красненьким светится ошибка CORS (так горячо любимая начинающими WEB-разработчиками):

Длинный код CORS ошибки

Access to fetch at 'http://127.0.0.1:3000/csrf-token' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.Understand this errorAI index.html:15

GET http://127.0.0.1:3000/csrf-token net::ERR_FAILED 200 (OK) (anonymous) @ index.html:15Understand this errorAI index.html:15

Uncaught TypeError: Failed to fetch at index.html:15:28

Исправление ошибки №2 с CORS

Попытаемся исправить ошибку с CORS:

  • установим пакет CORS командой npm i cors

  • изменим модуль index.js:

    • импортируем пакет cors: import cors from 'cors'

    • разрешим cors тотально: app.use(cors('*'));

Соответствующий коммит имеет название 'CORS'. Запускаем и видим текст на странице:

{"error":"csrf validation error"}

Но почему? Ведь все сделано верно, а csrf-csrf защита не работает.

Странность с Postman

Самое интересное, что с Postman все работает! Конкретно, запускаем сервер из демо примера и шлем на него запрос get из Postman: http://localhost:3000/csrf-token , получаем ответ, например, такой:
{"token":"a1c4452448cbee77c1e84d470aadc233eea117468de680505f833142f3abe23afd10c49cbc31abf1263ea8b8e33bcce38cd6749a285371ceca66a879d75d2460"}

Далее делаем post-запрос на http://localhost:3000/protected_endpoint :

Тело запроса

{ "name": "mauricio", "id": "xasd2312x2ñljkasdas" }

При этом если мы укажем заголовок x-csrf-token равный полученному из предыдущего get, то все работает:

{
"protected_endpoint": "form processed successfully"
}

Если же не укажем, или укажем ошибочный, то будет ошибка, авторизация не проходит:

{
"error": "csrf validation error"
}

Вопрос - почему через Postman все работает, а через браузер - нет?

Ошибка № 3 - детская

На этом этапе я обратился к наставнику за помощью. Первое, что он мне посоветовал - открывать index.html из примера ни как файл в браузере, а как файл на WEB-сервере (например, встроенном в VS Code "Go Live"). Открыл - не помогло. Но в комментариях в index.html прописал это нетривиальное для новичка требование.

Ошибка №4 - передача куки

Еще на этапе самостоятельной попытки запустить демо-пример столкнулся со странностями. Если логгирование сервера при запросах из Postman куки какие-то выводились, то при запросах из index.html куки были пустыми. Наставник сообщил, что если запрос fetch делается не просто из браузера, а из javascript, то куки по умолчанию не передаются. Зачем csrf-csrf куки, до конца не ясно. Но хочет, он их получит. Добавляем явную передачу куки в fetch запрос:

const response = await fetch(http://127.0.0.1:${PORT}/csrf-token,{credentials:"include"});

Как Вы думаете, заработало после этого? Ага, сейчас, прям разбежалось.

Ошибка № 5 - и снова CORS

В консоли браузера мы видим радостную ошибку CORS - не зря WEB-программисты его так любят, особенно начинающие.

Длинный код ошибки про CORS

Access to fetch at 'http://127.0.0.1:3000/csrf-token' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.Understand this errorAI index.html:58

GET http://127.0.0.1:3000/csrf-token net::ERR_FAILED 200 (OK) (anonymous) @ index.html:58Understand this errorAI index.html:58

Uncaught TypeError: Failed to fetch at index.html:58:28

Как с ней справиться? Наставник указал, что запросы с передачей куки разрешением всех cors (cors('*')) запрещены. Нужно явно указать origin. Указываем в сервере в модуле Index.js:

app.use(cors({origin:"http://127.0.0.1:5500"}));

Указали. Заработало? Ага, сейчас! Появилась новая ошибка CORS.

Ошибка №6 - и опять CORS

Вот какая наша новая ошибка CORS:

Еще одна длинная ошибка CORS

Access to fetch at 'http://127.0.0.1:3000/csrf-token' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.Understand this errorAI index.html:58

GET http://127.0.0.1:3000/csrf-token net::ERR_FAILED 200 (OK) (anonymous) @ index.html:58Understand this errorAI index.html:58

Uncaught TypeError: Failed to fetch at index.html:58:28

Опять таки наставник указал, что origin в настройках CORS должен идти вместе с credentials. Эти два сапога всегда ходят парой. Потому меняем в очередной раз модуль сервера index.js:

app.use(cors({origin:"http://127.0.0.1:5500", credentials: true}));

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

Ошибка №7 - все fetch!

Мы забыли что в демо-примере два fetch, добавим передачу куки и ко второму fetch в файле сервера index.js:

// The csrf cookie was implicit set on the request by the server
const post = await fetch(`http://127.0.0.1:${PORT}/protected_endpoint`, {
  method: "POST",
  headers: {
    "x-csrf-token": token, // comment this line to throw an error.
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "mauricio",
    id: "xasd2312x2ñljkasdas",
  }),
  credentials:"include"      
});

Проверяем - ура!!! Все работает:

{"protected_endpoint":"form processed successfully"}

Полная проверка

Для чистоты эксперимента закомментируем передачу заголовка в файле index.html:

//"x-csrf-token": token, // comment this line to throw an error.

И получаем ожидаемое:

{"error":"csrf validation error"}

Мои мысли, мои скакуны

7 ошибок в демо-примере из официальной документации!!! Как в том анекдоте из Советского Союза, в котором рекомендуется "доработать напильником".

Хотя конечно же, битва за работоспособность демо-примера лично мне позволила глубже понять внутренности WEB и csrf-уязвимости в частности. Однако, 7 ошибок - это перебор, сильно перебор!

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