Самый дорогой антипаттерн

Рассказывает автор блога m1el.github.io


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

«Самый дорогой антипаттерн» — не пустое сотрясание воздуха

Я подсчитал количество уязвимостей по их типам, используя данные с cve.mitre.org, и вот, что я получил:

  • rexec: 19268
  • DoS: 14849
  • xss: 9236
  • memory: 8212
  • sqlinj: 6230
  • privilege: 3321
  • dirtraversal: 2762
  • arith: 1260
  • csrf: 1117

Вы можете критиковать меня за то, как я это считал, и попробовать сделать это самостоятельно.

Вы можете заметить, что XSS (Cross Site Scripting — «межсайтовый скриптинг») и SQL инъекции представляют собой значительную часть списка. Я виню в большинстве уязвимостей этих типов именно антипаттерн printf. Пихать какие-то строки прямо в HTML — ужасная идея. С SQL то же самое.

Примеры использования на разных языках

Теперь, когда вы знаете значение этого антипаттерна, я докажу вам, что это буквально повсеместное явление. Вы можете думать, что он характерен только для HTML и SQL — именно поэтому и появились все эти уязвимости.

  • Буквально каждый сайт на PHP содержит что-то вроде:
  • SQL запрос сформированый с помощью строковых операций (см. документацию по mysql_query):
  • Многие программисты создают динамические элементы в JavaScript, используя что-то отдалённо напоминающее innerHtml (например, здесь):
  • Какой-нибудь бедный программист, которого заставили писать динамический сайт на C, может выдать:
  • Генерирование JSON с использованием строкового форматирования (например, здесь):
  • Использование sed для работы с XML:
  • Генерирование кода с помощью ExtJS (например, здесь):
  • MAL (Make-A-Lisp) задаёт load-file, как результат конкатенции строк:

    Define a load-file function using mal itself. In your main program call the rep function with this string: “(def! load-file (fn* (f) (eval (read-string (str “(do ” (slurp f) “)”)))))”.

Как же правильно обрабатывать данные?

И так, если я использую строковые функции для генерации HTML, что мне нужно использовать вместо этого?

Как вы можете видеть ниже – все предложенные решения имеют один смысл — использовать специальные методы создания объектов, а лишь потом сериализовать их. Главное – не обрабатывать сериализованные данные, как строки.

Для HTML

  • hiccup (больше примеров здесь):
  • lxml e-factory:
  • Работайте с DOM (Document Object Model), потом сериализуйте; простейший пример:
  • Используйте серверный AngularJS или React.
  • Управляйте DOM напрямую, например, через jQuery:

Для JSON

  • Используйте литералы или list compehensions для создания объекта JSON, потом сериализуйте его;
  • Генерируйте JSON объекты, а потом сериализуйте.

Для SQL

  • Используйте placeholder’ы (параметры запроса);
  • Работайте с Абстрактными Деревьями Синтаксиса (Abstract Syntax Trees – AST) SQL, а потом делайте запрос.

Для XML

  • Работайте с DOM, потом переводите в XML;
  • Пользуйтесь XSLT.

Для мета-программирования

  • Используйте Lisp;
  • Работайте с AST для вашего языка.

В чём корень проблемы?

На мой взгляд, дело в человеческой лени.

— О, мы можем создать разметку просто соединяя строки? Да, давайте так и сделаем!
— Но если строка будет содержать <img/src/onerror=alert(1)> это поведёт за собой выполнение кода JavaScript!
— Чёрт, ну давай напишем функцию htmlspecialchars и будем вызывать её каждый раз, когда нужно буде вставить текст в HTML…
— Но если написать <img class=" + htmlspecialchars($_GET['cls']) + " src="dot.gif">, JS всё равно сработает!
— Да только идиот будет писать такое.
— А что, если мы забудем вызвать эту функцию в каком-нибудь месте и у нас возникнут проблемы?
— Просто будем писать аккуратнее в следующий раз.

И вот, мы генерируем HTML, используя объединение строк. Конечно, если вы будете писать код очень, очень аккуратно, вы избежите уязвимостей. Но зачем вам такие проблемы? Вы же не используете ручное управление памятью для создания веб-сайта, правда? Нет смысла ходить по острию ножа, когда вы можете просто писать в безопасном ключе.

Немного о браузерах

У всей этой истории есть более весёлая сторона — браузеры. Их разработчики приспосабливали свои парсеры для работы с неправильным HTML, т.к. многие сайты содержали кривой код. Люди выбирали браузеры, которые могли воспринимать как HTML буквально произвольные наборы байтов и корректно отображали их любимые веб-сайты. Так же были разработаны XSS фильтры, т.к. сайты записывали необработанные запросы пользователей прямо в HTML. Суть таких фильтров проста — если браузер может предотвратить 90% XSS атак на пользователя, пользователь будет доволен. Так что, эти фильтры не могут защитить от всех атак.

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

В заключение

Структура HTML ужасна потому, что с самого начала люди стали управлять ей как строками. Очень много проблем (включая, но не ограничиваясь: XSS, некорректная разметка, разный парсинг браузеров) обусловлено неправильным пониманием сути формата HTML.

Предположим, — просто предположим, — что доступные инструменты не подталкивали бы людей к созданию HTML в виде строк — интернет тогда был бы лучше.

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

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

Перевод выполнил Пётр Соковых
Оригинал: «The Most Expensive Anti-Patternf»

Пётр Соковых, транслятор двоичного кода в русский язык