- Фундаментальная уязвимость HTML при встраивании скриптов
- Тег . Парсер HTML внутрь тега не заглядывает, для него это просто какой-то текст, который он потом отдает в парсер Javascript.
- Как эксплуатируется уязвимость
- А вы точно спецификация?
- Как избежать проблем?
- Как писать неподдерживаемый код?
- Соглашения – по настроению
- Пример из jQuery
- Краткость – сестра таланта!
- Именование
- Однобуквенные переменные
- Не используйте i для цикла
- Русские слова и сокращения
- Будьте абстрактны при выборе имени
- Похожие имена
- А.К.Р.О.Н.И.М
- Хитрые синонимы
- Словарь терминов – это еда!
- Повторно используйте имена
- Добавляйте подчёркивания
- Покажите вашу любовь к разработке
- Перекрывайте внешние переменные
- Мощные функции!
- Внимание… Сюр-при-из!
- Заключение
Фундаментальная уязвимость HTML при встраивании скриптов
Чтобы описать суть проблемы, мне нужно рассказать, как вообще устроен HTML. Вы наверняка в общих чертах представляли себе, но я все равно коротко пробегусь по основным моментам, которые понадобятся для понимания. Если кому-то не терпится, сразу переходите к сути.
HTML — это язык гипертекстовой разметки. Чтобы говорить на этом языке, нужно соблюдать его формат, иначе тот, кто читает написанное, не сможет вас понять. Например, в HTML у тегов есть атрибуты:
Тут [name] — это имя атрибута, а [value] — это его значение. В статье я буду использовать квадратные скобки вокруг кода, чтобы было понятно, где он начинается и заканчивается. После имени стои́т знак равенства, а после него — значение, заключенное в кавычки. Значение атрибута начинается сразу после первого символа кавычки и заканчивается сразу перед следующим символом кавычки, где бы он не находился. Это значит, что если вместо [value] вы запишете [OOO «Рога и копыта».] , то значение атрибута name будет [OOO ] , а еще у вашего элемента будет три других атрибута с именами: [рога] , [и] и [копыта».»] , но без значений.
Если это не то, чего вы ожидали, вам нужно как-то изменить значение атрибута, чтобы в нем не встречалась кавычка. Самое простое, что можно придумать — просто вырезать кавычки.
Тогда парсер HTML верно прочтет значение, но беда в том, что это будет другое значение. Вы хотели [OOO «Рога и копыта»] , а получили [OOO Рога и копыта.] . В каких-то случаях такое различие может быть критичным.
Чтобы вы могли указать в качестве значения любую строку, формат языка HTML предлагает возможность экранировать значения атрибутов. Вместо кавычки в строке значения вы можете записать последовательность символов [«] и парсер поймет, что в этом месте в исходной строке, которую вы хотите использовать в качестве значения атрибута, была кавычка. Такие последовательности называются HTML entities.
При этом, если в вашей исходной строке действительно была последовательность символов [«] , у вас все еще есть возможность записать её так, чтобы парсер не превратил её в кавычку — для этого надо заменить знак [&] на последовательность символов [&] , то есть вместо [«] вам нужно будет записать в сыром тексте ["] .
Получается, что преобразование из исходной строки в ту, которую мы запишем между двумя символами кавычек, является однозначным и обратимым. Благодаря этим преобразованиям можно записать и прочитать любую строку в качестве атрибута HTML-тега, не вдаваясь в суть её содержимого. Вы просто соблюдаете формат, и все работает.
Собственно, так работает большинство форматов, с которыми мы сталкиваемся: есть синтаксис, есть способ экранирования контента от этого синтаксиса и способ экранирования символов экранирования, если вдруг такая последовательность встречается в исходной строке. Большинство, но не…
Тег . Парсер HTML внутрь тега не заглядывает, для него это просто какой-то текст, который он потом отдает в парсер Javascript.
В свою очередь, Javascript — это самостоятельный язык с собственным синтаксисом, он, вообще говоря, никаким специальным образом не рассчитан на то, что будет встроен в HTML. В нем, как в любом другом языке, есть строковые литералы, в которых может быть что угодно. И, как вы уже должны были догадаться, может встретиться последовательность символов, означающая закрывающий тег .
Что тут должно происходить: переменной s должна присваиваться безобидная строка.
Что тут происходит на самом деле: Скрипт, в котором объявляется переменная s на самом деле заканчивается так: [var s = «surprise!] , что приводит к ошибке синтаксиса. Весь текст после него интерпретируется как чистый HTML и в него может быть внедрена любая разметка. В данном случае открывается новый тег ни в каком виде. А стандарт Javascript не запрещает такой последовательности быть где угодно в строковых литералах.
Получается парадоксальная ситуация: после встраивания валидного Javascript в валидный документ HTML абсолютно валидными средствами мы можем получить невалидный результат.
На мой взгляд это и является уязвимостью разметки HTML, приводящей к уязвимостям в реальных приложениях.
Как эксплуатируется уязвимость
Конечно, когда вы просто пишете какой-то код, трудно представить, что вы напишете в строке и не заметите проблем. Как минимум, подсветка синтаксиса даст вам знать, что тег закрылся раньше времени, как максимум, написанный вами код не запустится и вы будете долго искать, что произошло. Но это не является основной проблемой с этой уязвимостью. Проблема возникает там, где вы вставляете какой-то контент в Javascript, когда генерируете HTML. Вот частый кусок кода приложений на реакте с серверным рендерингом:
В initialState может появиться в любом месте, где данные поступают от пользователя или из других систем. JSON.stringify не будет менять такие строки при сериализации, потому что они полностью соответствуют формату JSON и Javascript, поэтому они просто попадут на страницу и позволят злоумышленнику выполнить произвольный Javascript в браузере пользователя.
Тут в строки с соответствующим экранированием записываются id пользователя и referer , который пришел на сервер. И, если в user.id вряд ли будет что-то кроме цифр, то в referer злоумышленник может запихнуть что угодно.
Но на закрывающем теге приколы не заканчиваются. Опасность представляет и открывающий тег
Что видит здоровый человек и большинство подсветок синтаксиса в этом коде? Два тега ниже, хе-хе). Если вы до этого не сталкивались с подобным, то можете подумать, что я сейчас шучу. К сожалению, нет. Вот скриншот DOM-дерева примера выше:
Самое неприятное, что в отличие от закрывающего тега , который в Javascript может встретиться только внутри строковых литералов, последовательности символов и
А вы точно спецификация?
Спецификация HTML, помимо того, что запрещает использование легальных последовательностей символов внутри тега «‘; console.log(script.outerHTML); >>> «
Как видите, строка с сериализованным элементом не будет распаршена в элемент, аналогичный исходному. Преобразование DOM-дерево → HTML-текст в общем случает не является однозначным и обратимым. Некоторые DOM-деревья просто нельзя представить в виде исходного HTML-текста.
Как избежать проблем?
Как вы уже поняли, способа безопасно вставить Javascript в HTML нет. Но есть способы сделать Javascript безопасным для вставки в HTML (почувствуйте разницу). Правда для этого нужно быть предельно внимательным всё время, пока вы пишете что-то внутри тега
Точно так же можно экранировать и отдельные строки.
Другой совет — не встраивайте в тег «>
Но, по-хорошему, конечно, если вы хотите нормально разрабатывать приложения, а не аккуратно ходить по минному полю, нужен надежный способ встраивания скриптов в HTML. Поэтому правильным решением считаю вообще отказаться от тега
Код внутри выглядит ужасно и непривычно. Но это код, который попадет в сам HTML. В шаблонизаторе, который вы используете, можно сделать простой фильтр, который будет вставлять тег и экранировать все его содержимое. Вот так может выглядеть код в шаблонизаторе Django:
Источник
Как писать неподдерживаемый код?
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/ninja-code.
Эта статья представляет собой мой вольный перевод How To Write Unmaintainable Code («как писать неподдерживаемый код») с дополнениями, актуальными для JavaScript.
Возможно, в каких-то из этих советов вам даже удастся узнать «этого парня в зеркале».
Предлагаю вашему вниманию советы мастеров древности, следование которым создаст дополнительные рабочие места для JavaScript-разработчиков.
Если вы будете им следовать, то ваш код будет так сложен в поддержке, что у JavaScript’еров, которые придут после вас, даже простейшее изменение займёт годы оплачиваемого труда! А сложные задачи оплачиваются хорошо, так что они, определённо, скажут вам «Спасибо».
Более того, внимательно следуя этим правилам, вы сохраните и своё рабочее место, так как все будут бояться вашего кода и бежать от него…
…Впрочем, всему своя мера. При написании такого кода он не должен выглядеть сложным в поддержке, код должен быть таковым.
Явно кривой код может написать любой дурак. Это заметят, и вас уволят, а код будет переписан с нуля. Вы не можете такого допустить. Эти советы учитывают такую возможность. Да здравствует дзен.
Соглашения – по настроению
Рабочий-чистильщик осматривает дом:
«…Вот только жук у вас необычный…
И чтобы с ним справиться, я должен жить как жук, стать жуком, думать как жук.»
(грызёт стол Симпсонов)
Сериал «Симпсоны», серия Helter Shelter
Чтобы помешать другому программисту исправить ваш код, вы должны понять путь его мыслей.
Представьте, перед ним – ваш большой скрипт. И ему нужно поправить его. У него нет ни времени ни желания, чтобы читать его целиком, а тем более – досконально разбирать. Он хотел бы по-быстрому найти нужное место, сделать изменение и убраться восвояси без появления побочных эффектов.
Он рассматривает ваш код как бы через трубочку из туалетной бумаги. Это не даёт ему общей картины, он ищет тот небольшой фрагмент, который ему необходимо изменить. По крайней мере, он надеется, что этот фрагмент будет небольшим.
На что он попытается опереться в этом поиске – так это на соглашения, принятые в программировании, об именах переменных, названиях функций и методов…
Как затруднить задачу? Можно везде нарушать соглашения – это помешает ему, но такое могут заметить, и код будет переписан. Как поступил бы ниндзя на вашем месте?
…Правильно! Следуйте соглашениям «в общем», но иногда – нарушайте их.
Тщательно разбросанные по коду нарушения соглашений с одной стороны не делают код явно плохим при первом взгляде, а с другой – имеют в точности тот же, и даже лучший эффект, чем явное неследование им!
Пример из jQuery
Этот пример требует знаний jQuery/DOM, если пока их у вас нет – пропустите его, ничего страшного, но обязательно вернитесь к нему позже. Подобное стоит многих часов отладки.
Во фреймворке jQuery есть метод wrap, который обёртывает один элемент вокруг другого:
Результат кода после операции wrap – два элемента, один вложен в другой:
А что же после append ?
Можно предположить, что добавится в конец div , сразу после img … Но ничего подобного!
Искусный ниндзя уже нанёс свой удар и поведение кода стало неправильным, хотя разработчик об этом даже не подозревает.
Как правило, методы jQuery работают с теми элементами, которые им переданы. Но не здесь!
Внутри вызова img.wrap(div) происходит клонирование div и вокруг img оборачивается не сам div , а его клон. При этом исходная переменная div не меняется, в ней как был пустой div , так и остался.
В итоге, после вызова получается два независимых div’а : первый содержит img (этот неявный клон никуда не присвоен), а второй – наш span .
Объяснения не очень понятны? Написано что-то странное? Это просто разум, привыкший, что соглашения уважаются, не допускает мысли, что вызов wrap – неявно клонирует элемент. Ведь другие jQuery-методы, кроме clone этого не делают.
Как говорил Учитель: «В древности люди учились для того, чтобы совершенствовать себя. Нынче учатся для того, чтобы удивить других».
Краткость – сестра таланта!
Пишите «как короче», а не как понятнее. «Меньше букв» – уважительная причина для нарушения любых соглашений.
Ваш верный помощник – возможности языка, использованные неочевидным образом.
Обратите внимание на оператор вопросительный знак ‘?’ , например:
Разработчик, встретивший эту строку и попытавшийся понять, чему же всё-таки равно i , скорее всего придёт к вам за разъяснениями. Смело скажите ему, что короче – это всегда лучше. Посвятите и его в пути ниндзя. Не забудьте вручить Дао дэ цзин.
Именование
Существенную часть науки о создании неподдерживаемого кода занимает искусство выбора имён.
Однобуквенные переменные
Называйте переменные коротко: a , b или c .
В этом случае никто не сможет найти её, используя фунцию «Поиск» текстового редактора.
Более того, даже найдя – никто не сможет «расшифровать» её и догадаться, что она означает.
Не используйте i для цикла
В тех местах, где однобуквенные переменные общеприняты, например, в счётчике цикла – ни в коем случае не используйте стандартные названия i , j , k . Где угодно, только не здесь!
Остановите свой взыскательный взгляд на чём-нибудь более экзотическом. Например, x или y .
Эффективность этого подхода особенно заметна, если тело цикла занимает одну-две страницы (чем длиннее – тем лучше).
В этом случае заметить, что переменная – счётчик цикла, без пролистывания вверх, невозможно.
Русские слова и сокращения
Если вам приходится использовать длинные, понятные имена переменных – что поделать… Но и здесь есть простор для творчества!
Назовите переменные «калькой» с русского языка или как-то «улучшите» английское слово.
В одном месте напишите var ssilka , в другом var ssylka , в третьем var link , в четвёртом – var lnk … Это действительно великолепно работает и очень креативно!
Количество ошибок при поддержке такого кода увеличивается во много раз.
Будьте абстрактны при выборе имени
Лучший кувшин лепят всю жизнь.
Высокая музыка неподвластна слуху.
Великий образ не имеет формы.
При выборе имени старайтесь применить максимально абстрактное слово, например obj , data , value , item , elem и т.п.
Идеальное имя для переменной: data . Используйте это имя везде, где можно. В конце концов, каждая переменная содержит данные, не правда ли?
Но что делать, если имя data уже занято? Попробуйте value , оно не менее универсально. Ведь каждая переменная содержит значение.
Занято и это? Есть и другой вариант.
Называйте переменную по типу данных, которые она хранит: obj , num , arr …
Насколько это усложнит разработку? Как ни странно, намного!
Казалось бы, название переменной содержит информацию, говорит о том, что в переменной – число, объект или массив… С другой стороны, когда непосвящённый будет разбирать этот код – он с удивлением обнаружит, что информации нет!
Ведь как раз тип легко понять, запустив отладчик и посмотрев, что внутри. Но в чём смысл этой переменной? Что за массив/объект/число в ней хранится? Без долгой медитации над кодом тут не обойтись!
Что делать, если и эти имена кончились? Просто добавьте цифру: item1, item2, elem5, data1 …
Похожие имена
Только истинно внимательный программист достоин понять ваш код. Но как проверить, достоин ли читающий?
Один из способов – использовать похожие имена переменных, например data и date . Бегло прочитать такой код почти невозможно. А уж заметить опечатку и поправить её… Ммммм… Мы здесь надолго, время попить чайку.
А.К.Р.О.Н.И.М
Используйте сокращения, чтобы сделать код короче.
Например ie (Inner Element), mc (Money Counter) и другие. Если вы обнаружите, что путаетесь в них сами – героически страдайте, но не переписывайте код. Вы знали, на что шли.
Хитрые синонимы
Очень трудно найти чёрную кошку в тёмной комнате, особенно когда её там нет.
Чтобы было не скучно – используйте похожие названия для обозначения одинаковых действий.
Например, если метод показывает что-то на экране – начните его название с display.. (скажем, displayElement ), а в другом месте объявите аналогичный метод как show.. ( showFrame ).
Как бы намекните этим, что существует тонкое различие между способами показа в этих методах, хотя на самом деле его нет.
По возможности, договоритесь с членами своей команды. Если Вася в своих классах использует display.. , то Валера – обязательно render.. , а Петя – paint.. .
…И напротив, если есть две функции с важными отличиями – используйте одно и то же слово для их описания! Например, с print. можно начать метод печати на принтере printPage , а также – метод добавления текста на страницу printText .
А теперь, пусть читающий код думает: «Куда же выводит сообщение printMessage ?». Особый шик – добавить элемент неожиданности. Пусть printMessage выводит не туда, куда все, а в новое окно!
Словарь терминов – это еда!
Ни в коем случае не поддавайтесь требованиям написать словарь терминов для проекта. Если же он уже есть – не следуйте ему, а лучше проглотите и скажите, что так и былО!
Пусть читающий ваш код программист напрасно ищет различия в helloUser и welcomeVisitor и пытается понять, когда что использовать. Вы-то знаете, что на самом деле различий нет, но искать их можно о-очень долго.
Для обозначения посетителя в одном месте используйте user , а в другом visitor , в третьем – просто u . Выбирайте одно имя или другое, в зависимости от функции и настроения.
Это воплотит сразу два ключевых принципа ниндзя-дизайна – сокрытие информации и подмена понятий!
Повторно используйте имена
По возможности, повторно используйте имена переменных, функций и свойств. Просто записывайте в них новые значения.
Добавляйте новое имя только если это абсолютно необходимо.
В функции старайтесь обойтись только теми переменными, которые были переданы как параметры.
Это не только затруднит идентификацию того, что сейчас находится в переменной, но и сделает почти невозможным поиск места, в котором конкретное значение было присвоено.
Цель – максимально усложнить отладку и заставить читающего код программиста построчно анализировать код и конспектировать изменения переменных для каждой ветки исполнения.
Продвинутый вариант этого подхода – незаметно (!) подменить переменную на нечто похожее, например:
Программист, пожелавший добавить действия с elem во вторую часть функции, будет удивлён. Лишь во время отладки, посмотрев весь код, он с удивлением обнаружит, что оказывается имел дело с клоном!
Регулярные встречи с этим приёмом на практике говорят: защититься невозможно. Эффективно даже против опытного ниндзи.
Добавляйте подчёркивания
Добавляйте подчёркивания _ и __ к именам переменных. Желательно, чтобы их смысл был известен только вам, а лучше – вообще без явной причины.
Этим вы достигните двух целей. Во-первых, код станет длиннее и менее читаемым, а во-вторых, другой программист будет долго искать смысл в подчёркиваниях. Особенно хорошо сработает и внесёт сумятицу в его мысли, если в некоторых частях проекта подчёркивания будут, а в некоторых – нет.
В процессе развития кода вы, скорее всего, будете путаться и смешивать стили: добавлять имена с подчёркиваниями там, где обычно подчёркиваний нет, и наоборот. Это нормально и полностью соответствует третьей цели – увеличить количество ошибок при внесении исправлений.
Покажите вашу любовь к разработке
Пусть все видят, какими замечательными сущностями вы оперируете! Имена superElement , megaFrame и niceItem при благоприятном положении звёзд могут привести к просветлению читающего.
Действительно, с одной стороны, кое-что написано: super.. , mega.. , nice.. С другой – это не несёт никакой конкретики. Читающий может решить поискать в этом глубинный смысл и замедитировать на часок-другой оплаченного рабочего времени.
Перекрывайте внешние переменные
Находясь на свету, нельзя ничего увидеть в темноте.
Пребывая же в темноте, увидишь все, что находится на свету.
Почему бы не использовать одинаковые переменные внутри и снаружи функции? Это просто и не требует придумывать новых имён.
Зашедший в середину метода render программист, скорее всего, не заметит, что переменная user локально перекрыта и попытается работать с ней, полагая, что это результат authenticateUser() … Ловушка захлопнулась! Здравствуй, отладчик.
Мощные функции!
Не ограничивайте действия функции тем, что написано в её названии. Будьте шире.
Например, функция validateEmail(email) может, кроме проверки e-mail на правильность, выводить сообщение об ошибке и просить заново ввести e-mail.
Выберите хотя бы пару дополнительных действий, кроме основного назначения функции.
Главное – они должны быть неочевидны из названия функции. Истинный ниндзя-девелопер сделает так, что они будут неочевидны и из кода тоже.
Объединение нескольких смежных действий в одну функцию защитит ваш код от повторного использования.
Представьте, что другому разработчику нужно только проверить адрес, а сообщение – не выводить. Ваша функция validateEmail(email) , которая делает и то и другое, ему не подойдёт. Работодатель будет вынужден оплатить создание новой.
Внимание… Сюр-при-из!
Есть функции, название которых говорит о том, что они ничего не меняют. Например, isReady , checkPermission , findTags … Предполагается, что при вызове они произведут некие вычисления, или найдут и возвратят полезные данные, но при этом их не изменят. В трактатах это называется «отсутствие сторонних эффектов».
По-настоящему красивый приём – делать в таких функциях что-нибудь полезное, заодно с процессом проверки. Что именно – совершенно неважно.
Удивление и ошеломление, которое возникнет у вашего коллеги, когда он увидит, что функция с названием на is.. , check.. или find. что-то меняет – несомненно, расширит его границы разумного!
Ещё одна вариация такого подхода – возвращать нестандартное значение.
Ведь общеизвестно, что is.. и check.. обычно возвращают true/false . Продемонстрируйте оригинальное мышление. Пусть вызов checkPermission возвращает не результат true/false , а объект с результатами проверки! А чего, полезно.
Те же разработчики, кто попытается написать проверку if (checkPermission(..)) , будут весьма удивлены результатом. Ответьте им: «надо читать документацию!». И перешлите эту статью.
Заключение
Все советы выше пришли из реального кода… И в том числе от разработчиков с большим опытом.
Возможно, даже больше вашего, так что не судите опрометчиво 😉
- Следуйте нескольким из них – и ваш код станет полон сюрпризов.
- Следуйте многим – и ваш код станет истинно вашим, никто не захочет изменять его.
- Следуйте всем – и ваш код станет ценным уроком для молодых разработчиков, ищущих просветления.
Источник