Главная » Статьи » На страже безопасности JavaScript

На страже безопасности JavaScript

На страже безопасности JavaScript

От автора: как написать код, который не делает того, чего не должен. Иногда нас, разработчиков, просят выполнять много разных задач. Мне приходилось быть графическим дизайнером для создания CSS, антропологом, работающим с сорока языками I18N / L10N, или детективом, собирающим воедино историю логов, чтобы найти ошибку в устаревшем коде.

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

Заставьте язык работать на себя

Представьте, что вас попросили просмотреть этот код:

<a name="makelink-1"></a> /** Возвращает две строки: фрагмент HTML и URL. */ function makeLink(linkTextHTML, url) { return `<a href="${ url }">${ linkTextHTML }</a>`; }

Это делает то, что нужно, но все равно глубоко ошибочно.

// Что, если злоумышленник контролирует ввод? const x = '<script>alert(1)</script>'; const y = 'javascript:alert(1)'; console.log(makeLink(x, y)); // -> <a href="javascript:alert(1)"><script>alert(1)</script></a>

Это тесно связано с предположениями по безопасности кода, который его вызывает. Да, входные данные являются строками, но они перестают быть просто строками, когда браузер интерпретирует части как привилегированный JavaScript.

Когда у вас есть разграничение, определите типы разграничения. Многие программы должны различать строки, которые по сути являются расширениями программы, и строки, которые не являются таковыми.

/** Возвращает ссылку, как TrustedHTML, представленный текстом ссылки TrustedHTML и TrustedURL. */ function makeLink(linkTextHTML, url) { // Если linkTextHTML - это TrustedHTML, не изменяется. В противном случае выполняем очищение. linkTextHTML = TrustedHTML.escape(linkTextHTML); // Если url - это TrustedURL, не изменяется. // В противном случае разрешаем это, если используется общая безопасная схема. url = TrustedURL.sanitize(TrustedURL); // Нам нужно обращать внимание на код, который создает TrustedHTML. return toTrustedHTML(`<a href="${ url }">${ linkTextHTML }</a>`); }

Это более многословно, но данная функция не тесно связана с непроверенными предположениями безопасности клиентов. (Позже я покажу, как сохранить код минимальным.)

Доверенные процессы не фильтруются

Представьте, что вы отвечаете за помощь разработчикам в создании безопасного кода. Вы бы предпочли услышать:

> Мы доверяем значениям, которые совпадают с разработанными логическими выражениями. <br> > Например, функция `> > > makeLink`> > > безопасна, потому что ее вывод соответсвует `> > > /^<a href="(?=https?:|mailto:|\w*(?![:\w]))[^"]*">[^<]*<\/a>$/`> > > . <br> > Так как наш код развивается, мы изменяем логическое значение.

или же

Мы доверяем выводу надежных инструментов, которыми пользуются доверенные разработчики. По мере развития нашего кода мы будем комбинировать эти инструменты новыми способами и тщательно проверять любые новые инструменты.

Первый аргумент основан на фильтрации. Это действительно трудно понять правильно (подсказка: в HTML ‘:’ и ‘\:’ означают одно и то же), и, поскольку программа развивается, фильтры, как правило, со временем ослабевают, поскольку во время тестирования ложные срабатывания легче найти, чем ложно-отрицательные.

Второй аргумент — это безопасность, исходя из конструкции, которая гораздо более гибкая. Проще сбалансировать функции и безопасность, когда вводу доверяют, из-за того, как откуда он исходит, а не из-за того, как расположены его биты.

Проверьте предположения, прежде чем сделать то, что вы не сможете отменить

Чтобы найти основную причину уязвимостей, инженеры по безопасности ищут критические точки: небольшое количество функций, которые вызываются, когда программа делает то, чего не должна, и которые нельзя отменить.

// Например, на сервере { response.write(x); } // нельзя отменить. // and on the client myDOMNode.innerHTML = x; // нельзя отменить.

Будет беда, если злоумышленник сможет вызвать:

x = '<img onerror="alert(1)" src="bogus">';

Программа, которая хранит ненадежные значения вне этих критических точек, является более надежной. Система типов может помочь нам избежать путаницы с надежными и ненадежными строками.

if (!TrustedHTML.is(x)) { throw new TypeError('Expected TrustedHTML but got ' + util.inspect(x)); }

Кстати, предложение по Надежным типам идентифицирует критические точки в DOM, и добавляет патчи для выполнения проверок вокруг этих точек. В Google мы используем JSConformance, чтобы увести разработчиков от этих критических точек и предоставить безопасные оболочки.

Используйте инструменты, на которые вы можете полагаться

Пример makeLink(…) немного причудливый. (Автор хотел, чтобы один пример охватывал всю статью.)

<a href="{{ url }}">{{ linkText }}</a>

Это намного лучше. Такие инструменты, как языки шаблонов, делают нашу жизнь проще, но если язык шаблонов банально добавляет строки, то это не лучше, чем первый пример makeLink.

Если язык шаблонов HTML не может создать простую ссылку без риска XSS, то это плохой инструмент.

Языки шаблонов, которые понимают различные контексты, в которых фигурируют url и linkText, могут принимать во внимание ваши решения при использовании TrustedHTML или TrustedURL, обеспечивая при этом безопасность при работе с простыми строками.

HTML не единственный язык, который создают программы Node. В «Дорожной карте по безопасности Node.js» я написал, что шаблоны строк с тегами обеспечивают баланс безопасности и простоты использования.

Например, safesql понимает синтаксис строк SQL, а тег sh понимает синтаксис bash и sh, поэтому вы можете использовать его вывод с child_process.

const { mysql } = require('safesql'); // ID автоматически очищается, используя соглашение SQL. const whereClause = mysql`WHERE id=${ id }`; // Вывод является доверенной строкой SQL, поэтому дополнительно не очищеатся. const query = mysql`SELECT * FROM Table ${ whereClause }`; const { sh } = require('sh-template-tag'); const shellCommand = sh`tar xfz ${ tarball }`;

Запланированные ошибки

Никто из нас не идеален. Я делаю больше ошибок, когда устаю, и иногда забываю вернуться к коду, который я планировал переработать. Если вы чувствуете себя в безопасности только тогда, когда ни один из членов команды не совершает ошибку, ищите другой подход.

Предоставление привилегий модуля позволяет команде коллективно сохранять небольшой объем кода, который может создавать значения Trusted , а владельцы кода могут помочь команде обеспечить дополнительный обзор изменений в этом небольшом объеме кода. Эти практики могут помочь обеспечить дополнительную проверку, где она необходима постоянно.

Но наличие нескольких пар глаз не помогает, если обзор становится утомительным. Давайте еще раз рассмотрим наш предыдущий пример кода:

/ ** С учетом некоторого TrustedHTML и TrustedURL возвращает ссылку в качестве TrustedHTML. * / function makeLink ( linkTextHTML , url ) { // Если linkTextHTML имеет тип TrustedHTML, он не изменяется. В противном случае очищаем его. linkTextHTML = TrustedHTML . escape ( linkTextHTML ); ... }

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

В идеале код, обеспечивающий свойства безопасности, тщательно проверяется, что означает, что его нужно менять редко. Если вы заметили слишком много сбоев, попробуйте переместить проверки безопасности в код более низкого уровня с более низким уровнем сбоев. В идеале в фреймворк или плагин фреймворка.

Через уязвимый код не может утечь то, чего он не содержит

Представьте, что ваш коллега пишет веб-сервис, который выдает ответ JSON, когда пользователь запрашивает свой список друзей.

Они тестируют веб-сервис и видят, что он производит массив вроде [ { «displayName»: «ILikeCats», «userId»: «A01F» } ]. Позже я хочу сделать более подробный ответ для текущего пользователя. Я добавляю поле homeAddress к тому же классу Account, что использовал мой коллега.

Мы склонны полагать, что добавление в API не нарушит существующий код, но мое добавочное изменение теперь приводит к утечке через запрос коллеги PII: [ { «displayName»: «ILikeCats», «userId»: «A01F», «homeAddress»: «…» } ]

В «Багах свойств» рассказывается об этой проблеме и о том, как избежать непреднамеренных утечек через символы и поля.

Относитесь к JavaScript как к динамическому языку

Статические типы TypeScript помогают своевременно обнаруживать ошибки, но их недостаточно для безопасности. Вы можете увидеть проверки во второй версии makeLink и испытать искушение использовать статические типы вместо проверок во время выполнения:

function makeLink(linkText: TrustedHTML, url: TrustedURL): TrustedHTML { return toTrustedHTML(`<a href="${ url }">${ linkTextHTML }</a>`); }

Это прекрасное изменение, но вы не должны удалять код, который проверяет входные данные. TypeScript делает прагматические предположения, так что, даже если нет ошибок типа, значение переменной может не соответствовать ее типу. Если вы не уверены (или вы фанат TS):

Факты формы определяют функцию, которая возвращает число, за исключением случаев, когда злоумышленник использует его для загрузки произвольного кода.

Несоответствие типов определяет «доверенный» класс типов, но TypeScript не распознает нарушения типов.

«Очень простой фрагмент кода» предлагает вам попытаться выяснить, как атаковать фрагмент JS, который все еще уязвим при добавлении типов.

Уровень защиты

Я использовал везде XSS в качестве примера проблемы безопасности, так как разработчики знакомы с ним, но некоторые могут сказать: «Почему я должен беспокоиться о XSS? Я использую строгую Content-Security-Policy».

Я бы не сказал: «Я ношу защитный шлем, поэтому мне не нужны защитные очки». Когда вы создаете защитные слои, в данном случае Content-Security-Policy, и безопасные методы кодирования, злоумышленник должен обойти и то, и другое, чтобы повлиять на ваших пользователей.

Попросите помощи

Компьютерная безопасность — это отдельная область с собственными руководствами, так что не ожидайте, что вы будете знать все. Мы, люди, занимающиеся безопасностью, может показаться немного параноиками, но мы действительно дружелюбны, поэтому не стесняйтесь обращаться.

Источник: https://www.javascriptjanuary.com/

Редакция: Команда webformyself.