Заставляем работать демонстрационный пример из официальной документации npm пакета 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 сейчас
А поскольку мне нужна была 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 ошибок — это перебор, сильно перебор!