Неудивительно, что веб-разработчики упускают из вида некоторые нюансы. С другой стороны, злоумышленники могут использовать особенности Юникода в своих целях, что и делают.
Специалист по безопасности Джон Грейси продемонстрировал на примере GitHub баг проверки адреса электронной почты для восстановления забытого пароля. Подобные баги можно встретить и на других сайтах.
Джон Грейси объясняет, что такое «коллизия трансляции знаков», когда два разных знака после конвертации транслируются в один и тот же знак.
В данном случае он использовал турецкий символ '?' ('i' без точки), который транслируется в латинскую 'i', так что почтовый адрес
John@G?thub.com
после обработки превращается в John@Github.com
:'?'.toLowerCase() // 'ss'
'?'.toLowerCase() === 'SS'.toLowerCase() // true
// Note the Turkish dotless i
'John@G?thub.com'.toUpperCase() === 'John@Github.com'.toUpperCase()
Такие коллизии можно найти по всем плоскостям Юникода: вот полный список.
Нас интересуют в первую очередь те знаки, которые транслируются в латинские символы. Таких всего одиннадцать вариантов. На третьем месте в таблице как раз турецкий знак 'i' без точки.
Знак | Кодовая точка | Результат |
---|---|---|
? | 0x00DF | SS |
? | 0x0131 | I |
? | 0x017F | S |
? | 0xFB00 | FF |
? | 0xFB01 | FI |
? | 0xFB02 | FL |
? | 0xFB03 | FFI |
? | 0xFB04 | FFL |
? | 0xFB05 | ST |
? | 0xFB06 | ST |
? | 0x212A | k |
GitHub позволял злоумышленнику получить пароль от чужого аккаунта, потому что процедура восстановления забытого пароля работала некорректно.
В рамках этой процедуры выполнялось сравнение введённого адреса электронной почты с адресом, который хранится в базе. Алгоритм проверки:
- Введённый адрес переводится в нижний регистр с помощью метода toLowerCase.
- Введённый адрес сравнивается с адресом в базе зарегистрированных пользователей.
- Если найдено совпадение, пароль из базы данных высылается на введённый адрес.
Очевидно, разработчики не знали о коллизии трансляции адресов при использовании метода
toLowerCase
.В данном случае исправить ошибку просто. Достаточно высылать пароль не на введённый адрес, а на адрес из базы данных.
Конечно, это не полное исправление ошибки, а только быстрый патч. Более полным решением будет трансляция в Punycode для проверки:
John@G?thub.com
> xn—john@gthub-2ub.com
. Punycode был разработан для однозначного преобразования доменных имен в последовательность ASCII-символов. Адрес электронной почты можно проверять таким же способом, но большинство веб-приложений этого не делает.За найденную уязвимость Джон Грейси получил денежное вознаграждение и 2500 очков в рейтинг, хотя ему ещё далеко до главного гитхабовского хакера Александра Добкина <img src=404 onerror=alert(document.domain)>: пользователь с таким необычным именем заработал уже 30 750 очков, в том числе за выполнение произвольного кода на серверах GitHub, на которых генерируются страницы GitHub Pages.
Сбой в мессенджере при получении эмодзи с чёрной точкой (Messenger в iOS, WhatsApp под Android)
Связанные с Юникодом баги имеют такое свойство, что их можно встретить в любом приложении, которое обрабатывает текст, введённый пользователем. Уязвимости есть и в веб-приложениях, и в нативных программах под Android и iOS. Одним из самых известных стал баг iOS от 2015 года, когда несколько знаков Юникода в текстовом сообщении вызывали сбой операционной системы. В прошлом году похожий юникодовский баг обнаружили в iOS 11.3, он известен как «чёрная точка». Похожий сбой происходил в приложении WhatsApp под Android, если прикоснуться к эмодзи.
Комментарии (57)
ATwn
20.12.2019 22:13+1Мало кто знает все хитрости
Спасибо за ссылку на статью! Берём на вооружение :)
kafeman
20.12.2019 23:33
У кого-нибудь это работает? У меня почему-то получается'?'.toLowerCase() === 'SS'.toLowerCase() // true
false
.Co0l3r
21.12.2019 01:18+1похоже, что имелось ввиду
'?'.toUpperCase() == 'ss'.toUpperCase()
E_STRICT
21.12.2019 10:09Для LowerCase тоже есть одна коллизия.
eng.getwisdom.io/awesome-unicode/#lowercasetransformationcollisions
ivan386
21.12.2019 00:08+2Это
John@G?thub.com > xn--john@gthub-2ub.com
кажись должно быть такJohn@G?thub.com > john@xn--gthub-n4a.com
но punycoder делает первый вариант.bano-notit
21.12.2019 18:10Punycode умеет только строки переводить. Он не знает где у мыла домен, а где логин. В логине может быть всё что угодно, там делать какие-либо изменения не корректно в принципе. А вот к домену какие-то требования применять можно.
HellWalk
21.12.2019 10:45А в PHP данный баг работает?
Выполняю:
var_dump(strtolower('?')); // ?
Коллизии нетFTOH
21.12.2019 12:05Выше написали, что автор ошибся. Нужно в верхний регистр переводить.
PHP по умолчанию умеет работать только с кодировкой ASCII (пример:strlen('я')
даст результат2
)
Для работы с Юникодом нужно подключить плагин mbstring. И правильный пример будет такой:
var_dump(mb_strtoupper('?'));
Ostrouschcko
21.12.2019 13:55var_dump(mb_strtoupper('?')); //string(2) "?"
Моя конфигурация mbstringphp -i | grep -i mbstring /etc/php/7.2/cli/conf.d/20-mbstring.ini, Zend Multibyte Support => provided by mbstring Multibyte decoding support using mbstring => enabled mbstring mbstring extension makes use of "streamable kanji code filter and converter", which is distributed under the GNU Lesser General Public License version 2.1. mbstring.detect_order => no value => no value mbstring.encoding_translation => Off => Off mbstring.func_overload => 0 => 0 mbstring.http_input => no value => no value mbstring.http_output => no value => no value mbstring.http_output_conv_mimetypes => ^(text/|application/xhtml\+xml) => ^(text/|application/xhtml\+xml) mbstring.internal_encoding => no value => no value mbstring.language => neutral => neutral mbstring.strict_detection => Off => Off mbstring.substitute_character => no value => no value
FTOH
21.12.2019 17:23+1Проверил через сайт http://sandbox.onlinephpfunctions.com, что если версия php меньше 7.3.5, то коллизия не проявляется.
CryInt
23.12.2019 13:15Так эта функция не работает с многобайтными кодировками, включая юникод. Используйте mb_strtolower. Но там написано что может преобразовывать, вот только не понятно правильно или нет.
ExplosiveZ
21.12.2019 13:26Зачем менять регистр почты или логина?
AllexIn
21.12.2019 13:48Почта не регистрозависимая.
Мог при регистрации ввести John, а при восстановлении забыть и писать john.johnfound
21.12.2019 13:58-2Адрес почты регистрозависим! По крайней мере до знака @.
arthuriantech
21.12.2019 14:30+1Нет. Локальная часть до @ интерпретируется сервером. Домен к регистру не чувствителен.
https://tools.ietf.org/html/rfc5321#section-2.3.11bano-notit
21.12.2019 18:13+1Так вы ссылку кинули как раз на документ, который говорит, что после @ по факту регистронезависим, а до собаки — зависит от реализации сервера. К чему тут "нет"?
AllexIn
21.12.2019 19:57нет тут к тому, что общепринято делать почту не регистрозависимой.
bano-notit
21.12.2019 20:00Давайте по честному. Не принято, а легче так думать и так её обрабатывать. Но из этого следует не то, что можно это считать стандартом де факто, а то, что просто всем лень её обрабатывать правильно.
AllexIn
21.12.2019 20:02Лень не имеет никакого отношения к реальности в этой ситуации.
Просто почтовик, который разрешит регистрировать разные адреса отличающиеся только регистром и сортировать почту с учетом регистра никто не будет использовать.
Потому что преимуществ ровно ноль, а вот проблем посыпется с головой.
Зачем условному админу почты может захотеться иметь регистрозависимою почту? Просто зачем?bano-notit
21.12.2019 20:05-1За тем, что логины могут быть и не только от почты, а браться например из совершенно другой системы? Например если бы хабр решил всем раздать мыльники с
@habr.com
, то он бы скорее всего взял в качестве имени ящика именно ник пользователя. А если их приводить к одному регистру, то начнутся проблемы.
И это синтетический.
Практический юзкейс — русские имена в почте.
gecube
21.12.2019 20:25+1русские имена в почте.
Такого не должно быть потому что не должно быть.
Например если бы хабр решил всем раздать мыльники с habr.com, то он бы скорее всего взял в качестве имени ящика именно ник
Вы правда думаете, что ники регистрозависимы?
bano-notit
21.12.2019 20:28+1Такого не должно быть потому что не должно быть.
В стандарте написано, что может быть что угодно, значит там будет что угодно.
Вы правда думаете, что ники регистрозависимы?
Смотря где. На хабре независимые, а в другой системе может быть что угодно.
AllexIn
21.12.2019 20:27Примеры весьма показательны тем, что абсолютно абстрактны и не имеют отношения к реальности.
Даже столкнувшись с такой задачей никто в здравом уме не влезет в такое legacy. Скорее выберут какое-то компромиссное решение.bano-notit
21.12.2019 20:33+1Скорее выберут какое-то компромиссное решение.
Это совершенно не значит, что какой-нибудь админ не засядет и не сделает всем русские мыльники. Точно так же это не значит, что в каком-то нормальном почтовике вдруг не реализуют полную поддержку стандарта этого.
Слишком много в этом всём деле "скорее всего", "вдруг" и "никто в здравом уме". Это подход, который как раз и тянет за собой опасности по типу "никакое устройство в здравом уме не будет нам подкидывать битые пакеты". А получается из этого много и много уязвимостей.
AllexIn
21.12.2019 20:49-1Я лишь утверждал что причиной является не лень.
Ну а проблемы людей, которые захостились на такой почте врядли стоит учитывать вообще, их слишком мало, даже в теории.
Проблема этих людей будет в первую очередь бить по хостеру решившему сделать дичь, а не по сервису считающему почту регистронезависимой, ибо это стандарт дефакто.bano-notit
21.12.2019 22:21У вас странное понимание выражения "стандарт де факто". Для меня это когда никаких бумаг нет, но все приняли какое-то общее решение. А тут бумага есть и она является стандартом. Так что никаких де факто тут быть не может.
AllexIn
21.12.2019 22:24Вот этот вот стандарт — это де юре.
А то что сделано и используется — де факто.
Хоть википедию бы открыли сначала чтоли. :)
arthuriantech
21.12.2019 23:48Адрес почты регистрозависим! По крайней мере до знака @.
Строго говоря, строка ДО знака @ НЕ является регистрозависимой или регистронезависимой. Это определено реализацией сервера. Строка ПОСЛЕ @ адресует сервер, и она является регистроНЕзависимой. Извините за мой французский.
johnfound
21.12.2019 13:56Конечно, обрабатывать UNICODE не просто.
Но дело в том, что программисты часто обрабатывают то, чего не надо обрабатывать совсем.
Пример с email показателен! Почтовые адреса чувствительны к регистру! Зачем надо было преобразовать? Сравнивайте напрямую. Потребитель который хочет сменить пароль очень хорошо знает какой у него адрес!
И вообще, тема валидации почтовых адресов совершенно неоднозначная. И в ней правило "меньше лучше" действует на все 100%.
xlenz
21.12.2019 14:18Нигде не смог найти подтверждения того, что эмейл адрес должен быть чувствителен к регистру. Можно смылку, пожалуйста?
Bronx
21.12.2019 15:23+3
The standard mailbox naming convention is defined to be
local-part@domain
;… the local-part MUST be interpreted and assigned semantics only by the host specified in the domain part of the address.Иными словами, это дело хоста решать, зависимы ли имена в его домене от регистра или нет. Клиент должен исходить из худшего, и хранить/посылать емейлы в том регистре, в каком его ввели изначально. Поиск среди сохранённых емейлов можно вести регистронезависимо.
gecube
21.12.2019 14:19Почтовые адреса чувствительны к регистру!
В нормальных почтовиках нет. И условный gecube@gmail.com будет то же самое, что и GECUBE@GMAIL.COM
gecube
21.12.2019 14:21Хуже того. G.E.C.U.B.E@GMAIL.COM может быть эквивалентом GECUBE@GMAIL.COM. Как и GECUBE+TAG@GMAIL.COM
psycho-coder
21.12.2019 15:05+1ЕМНИП: точки в имени это фишка гугла, а вот тэг это уже rfc
gecube
21.12.2019 15:06Но тем не менее — MS Exchange игнорирует обе возможности )
В остальном — Вы правы )psycho-coder
21.12.2019 15:35MS Exchange полностью игнорит тэги или можно включить? Zimbra по умолчаню игнорит, но включить можно.
gecube
21.12.2019 16:03MS Exchange полностью игнорит тэги или можно включить?
Насколько мне известно — полностью. Но я могу чего-то не знать :-)
johnfound
21.12.2019 18:16Что это за "нормальный почтовик"????
Регистрозависимость решает получатель, а не отправитель! И тот клиент, который отправляет почту по собственным правилам рискует эту почту просто не доставить. Если "почтовик" не доставивший почту, хотя это было вполне возможно, "нормальный", то спасибо, но нет. Возьму ненормального.
gecube
21.12.2019 18:26+1Так об этом и речь, что если на стороне получателя почтовый сервис различает юзеров в зависимости от регистра и позволяет регистрировать одинаковые, но разные почтовые адреса, то это первый шаг в сторону ада. Про сторону отправителя я ничего не говорил — как отправитель написал, так и письмо должно уйти. Благо обычно проблемы нет, т.к. почтовый адрес берется из известного источника — визитка, контакт, сайт и пр.
johnfound
22.12.2019 22:00Так об этом и речь, что если на стороне получателя почтовый сервис различает юзеров в зависимости от регистра и позволяет регистрировать одинаковые, но разные почтовые адреса, то это первый шаг в сторону ада.
Ну-у-у, ад-не-ад, а стандарт этого допускает. Так что вся обработка адресов (вкл. валидация) должна соответствовать. И кстати это по сути значительно упрощает обработку и делает жизнь легче.
Kwisatz
21.12.2019 14:08+2Введённый адрес переводится в нижний регистр с помощью метода toLowerCase.
Введённый адрес сравнивается с адресом в базе зарегистрированных пользователей.
Если найдено совпадение, пароль из базы данных высылается на введённый адрес.
Причем тут юникод не совсем понятно. Джуновская ошибка, старая как мир.
AbrikOS3
21.12.2019 15:03Так, вы сказали, что в Юникоде 216 символов, а там можно хранить 231, но используются только 1 112 064. Видимо вы перппутали с размером плоскости, который как раз 2**16 (информация из Википедии)
AbrikOS3
21.12.2019 15:08+1Такс, не учёл разметку. Имел ввиду, что в Юникоде можно хранить 2^31, а не 2^16, как вы написали.
CyclusVitalis
23.12.2019 13:15В php такого не наблюдаю при преобразовании UTF-8 строки.
CyclusVitalis
23.12.2019 15:07Пока комментарий одобряли, он устарел :(
Объяснение habr.com/ru/company/globalsign/blog/481318/#comment_21049044
EvgenT