Заставляем работать демонстрационный пример из официальной документации npm пакета csrf-csrf

Схема 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'. Запускаем и видим текст на странице:

{«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 ошибок — это перебор, сильно перебор!

© Habrahabr.ru