Юникод исключительно сложен. Мало кто знает все хитрости: от невидимых символов и контрольных знаков до суррогатных пар и комбинированных эмодзи (когда при сложении двух знаков получается третий). Стандарт включает 216 кодовых позиций в 17-ти плоскостях. По сути, изучение Юникода можно сравнить с изучением отдельного языка программирования.

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

Специалист по безопасности Джон Грейси продемонстрировал на примере 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 позволял злоумышленнику получить пароль от чужого аккаунта, потому что процедура восстановления забытого пароля работала некорректно.

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

  1. Введённый адрес переводится в нижний регистр с помощью метода toLowerCase.
  2. Введённый адрес сравнивается с адресом в базе зарегистрированных пользователей.
  3. Если найдено совпадение, пароль из базы данных высылается на введённый адрес.

Очевидно, разработчики не знали о коллизии трансляции адресов при использовании метода 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)


  1. EvgenT
    20.12.2019 15:17
    +2

    так что почтовый адрес John@G?thub.com после обработки превращается в John@G?thub.com:
    видимо во втором случае должно быть John@Github.com?


  1. ATwn
    20.12.2019 22:13
    +1

    Мало кто знает все хитрости

    Спасибо за ссылку на статью! Берём на вооружение :)


    1. Lsh
      20.12.2019 23:20

      Интересно, у многих ли этот адрес не открывается? Заблокирован?


      1. Vindicar
        21.12.2019 10:47

        Не думаю что заблокирован — через прокси тоже не открывается.


        1. DracoL1ch
          21.12.2019 11:33

          Открыл через vpn, заблокирован через ркн наверняка


  1. kafeman
    20.12.2019 23:33

    '?'.toLowerCase() === 'SS'.toLowerCase() // true
    У кого-нибудь это работает? У меня почему-то получается false.


    1. Co0l3r
      21.12.2019 01:18
      +1

      похоже, что имелось ввиду

      '?'.toUpperCase() == 'ss'.toUpperCase()


      1. E_STRICT
        21.12.2019 10:09

        Для LowerCase тоже есть одна коллизия.
        eng.getwisdom.io/awesome-unicode/#lowercasetransformationcollisions


  1. 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 делает первый вариант.


    1. bano-notit
      21.12.2019 18:10

      Punycode умеет только строки переводить. Он не знает где у мыла домен, а где логин. В логине может быть всё что угодно, там делать какие-либо изменения не корректно в принципе. А вот к домену какие-то требования применять можно.


  1. HellWalk
    21.12.2019 10:45

    А в PHP данный баг работает?

    Выполняю:

    var_dump(strtolower('?')); // ?

    Коллизии нет


    1. FTOH
      21.12.2019 12:05

      Выше написали, что автор ошибся. Нужно в верхний регистр переводить.
      PHP по умолчанию умеет работать только с кодировкой ASCII (пример: strlen('я') даст результат 2)
      Для работы с Юникодом нужно подключить плагин mbstring. И правильный пример будет такой:


      var_dump(mb_strtoupper('?'));


      1. Ostrouschcko
        21.12.2019 13:55

        var_dump(mb_strtoupper('?')); //string(2) "?"

        Моя конфигурация mbstring
        php -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


        1. FTOH
          21.12.2019 17:23
          +1

          Проверил через сайт http://sandbox.onlinephpfunctions.com, что если версия php меньше 7.3.5, то коллизия не проявляется.


          1. Ostrouschcko
            22.12.2019 22:18
            -1

            Спасибо, буду иметь ввиду



    1. CryInt
      23.12.2019 13:15

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


  1. Victor_koly
    21.12.2019 12:02

    del
    Вы указали в тексте решение проблемы.


  1. ExplosiveZ
    21.12.2019 13:26

    Зачем менять регистр почты или логина?


    1. AllexIn
      21.12.2019 13:48

      Почта не регистрозависимая.
      Мог при регистрации ввести John, а при восстановлении забыть и писать john.


      1. johnfound
        21.12.2019 13:58
        -2

        Адрес почты регистрозависим! По крайней мере до знака @.


        1. arthuriantech
          21.12.2019 14:30
          +1

          Нет. Локальная часть до @ интерпретируется сервером. Домен к регистру не чувствителен.
          https://tools.ietf.org/html/rfc5321#section-2.3.11


          1. bano-notit
            21.12.2019 18:13
            +1

            Так вы ссылку кинули как раз на документ, который говорит, что после @ по факту регистронезависим, а до собаки — зависит от реализации сервера. К чему тут "нет"?


            1. AllexIn
              21.12.2019 19:57

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


              1. bano-notit
                21.12.2019 20:00

                Давайте по честному. Не принято, а легче так думать и так её обрабатывать. Но из этого следует не то, что можно это считать стандартом де факто, а то, что просто всем лень её обрабатывать правильно.


                1. AllexIn
                  21.12.2019 20:02

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


                  1. bano-notit
                    21.12.2019 20:05
                    -1

                    За тем, что логины могут быть и не только от почты, а браться например из совершенно другой системы? Например если бы хабр решил всем раздать мыльники с @habr.com, то он бы скорее всего взял в качестве имени ящика именно ник пользователя. А если их приводить к одному регистру, то начнутся проблемы.
                    И это синтетический.


                    Практический юзкейс — русские имена в почте.


                    1. gecube
                      21.12.2019 20:25
                      +1

                      русские имена в почте.

                      Такого не должно быть потому что не должно быть.


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

                      Вы правда думаете, что ники регистрозависимы?


                      1. bano-notit
                        21.12.2019 20:28
                        +1

                        Такого не должно быть потому что не должно быть.

                        В стандарте написано, что может быть что угодно, значит там будет что угодно.


                        Вы правда думаете, что ники регистрозависимы?

                        Смотря где. На хабре независимые, а в другой системе может быть что угодно.


                    1. AllexIn
                      21.12.2019 20:27

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


                      1. bano-notit
                        21.12.2019 20:33
                        +1

                        Скорее выберут какое-то компромиссное решение.

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


                        Слишком много в этом всём деле "скорее всего", "вдруг" и "никто в здравом уме". Это подход, который как раз и тянет за собой опасности по типу "никакое устройство в здравом уме не будет нам подкидывать битые пакеты". А получается из этого много и много уязвимостей.


                        1. AllexIn
                          21.12.2019 20:49
                          -1

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


                          1. bano-notit
                            21.12.2019 22:21

                            У вас странное понимание выражения "стандарт де факто". Для меня это когда никаких бумаг нет, но все приняли какое-то общее решение. А тут бумага есть и она является стандартом. Так что никаких де факто тут быть не может.


                            1. AllexIn
                              21.12.2019 22:24

                              Вот этот вот стандарт — это де юре.
                              А то что сделано и используется — де факто.
                              Хоть википедию бы открыли сначала чтоли. :)


            1. arthuriantech
              21.12.2019 23:48

              Адрес почты регистрозависим! По крайней мере до знака @.

              Строго говоря, строка ДО знака @ НЕ является регистрозависимой или регистронезависимой. Это определено реализацией сервера. Строка ПОСЛЕ @ адресует сервер, и она является регистроНЕзависимой. Извините за мой французский.


  1. johnfound
    21.12.2019 13:56

    Конечно, обрабатывать UNICODE не просто.


    Но дело в том, что программисты часто обрабатывают то, чего не надо обрабатывать совсем.


    Пример с email показателен! Почтовые адреса чувствительны к регистру! Зачем надо было преобразовать? Сравнивайте напрямую. Потребитель который хочет сменить пароль очень хорошо знает какой у него адрес!


    И вообще, тема валидации почтовых адресов совершенно неоднозначная. И в ней правило "меньше лучше" действует на все 100%.


    1. xlenz
      21.12.2019 14:18

      Нигде не смог найти подтверждения того, что эмейл адрес должен быть чувствителен к регистру. Можно смылку, пожалуйста?


      1. Bronx
        21.12.2019 15:23
        +3

        RFC-5321:


        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.

        Иными словами, это дело хоста решать, зависимы ли имена в его домене от регистра или нет. Клиент должен исходить из худшего, и хранить/посылать емейлы в том регистре, в каком его ввели изначально. Поиск среди сохранённых емейлов можно вести регистронезависимо.


    1. gecube
      21.12.2019 14:19

      Почтовые адреса чувствительны к регистру!

      В нормальных почтовиках нет. И условный gecube@gmail.com будет то же самое, что и GECUBE@GMAIL.COM


      1. gecube
        21.12.2019 14:21

        Хуже того. G.E.C.U.B.E@GMAIL.COM может быть эквивалентом GECUBE@GMAIL.COM. Как и GECUBE+TAG@GMAIL.COM


        1. psycho-coder
          21.12.2019 15:05
          +1

          ЕМНИП: точки в имени это фишка гугла, а вот тэг это уже rfc


          1. gecube
            21.12.2019 15:06

            Но тем не менее — MS Exchange игнорирует обе возможности )
            В остальном — Вы правы )


            1. psycho-coder
              21.12.2019 15:35

              MS Exchange полностью игнорит тэги или можно включить? Zimbra по умолчаню игнорит, но включить можно.


              1. gecube
                21.12.2019 16:03

                MS Exchange полностью игнорит тэги или можно включить?

                Насколько мне известно — полностью. Но я могу чего-то не знать :-)


                1. psycho-coder
                  21.12.2019 16:59

                  Ясно)


          1. t38c3j
            21.12.2019 16:31

            Это не тэг а сабэдрисиз, и на них есть rfc


            1. psycho-coder
              21.12.2019 17:02

              Спасибо. Везде встречал их как «тэги»


      1. johnfound
        21.12.2019 18:16

        Что это за "нормальный почтовик"????


        Регистрозависимость решает получатель, а не отправитель! И тот клиент, который отправляет почту по собственным правилам рискует эту почту просто не доставить. Если "почтовик" не доставивший почту, хотя это было вполне возможно, "нормальный", то спасибо, но нет. Возьму ненормального.


        1. gecube
          21.12.2019 18:26
          +1

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


          1. johnfound
            22.12.2019 22:00

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

            Ну-у-у, ад-не-ад, а стандарт этого допускает. Так что вся обработка адресов (вкл. валидация) должна соответствовать. И кстати это по сути значительно упрощает обработку и делает жизнь легче.


  1. Kwisatz
    21.12.2019 14:08
    +2

    Введённый адрес переводится в нижний регистр с помощью метода toLowerCase.
    Введённый адрес сравнивается с адресом в базе зарегистрированных пользователей.
    Если найдено совпадение, пароль из базы данных высылается на введённый адрес.


    Причем тут юникод не совсем понятно. Джуновская ошибка, старая как мир.


  1. vp_arth
    21.12.2019 14:48
    +2

    Если найдено совпадение, пароль из базы данных высылается на введённый адрес.

    Надеюсь, что пароля в базе данных никогда не было, а высылается токен авторизации процедуры смены пароля…


    1. gecube
      21.12.2019 15:05

      Причем токен авторизации имеет срок действия )


  1. AbrikOS3
    21.12.2019 15:03

    Так, вы сказали, что в Юникоде 216 символов, а там можно хранить 231, но используются только 1 112 064. Видимо вы перппутали с размером плоскости, который как раз 2**16 (информация из Википедии)


    1. AbrikOS3
      21.12.2019 15:08
      +1

      Такс, не учёл разметку. Имел ввиду, что в Юникоде можно хранить 2^31, а не 2^16, как вы написали.


  1. CyclusVitalis
    23.12.2019 13:15

    В php такого не наблюдаю при преобразовании UTF-8 строки.


    1. CyclusVitalis
      23.12.2019 15:07

      Пока комментарий одобряли, он устарел :(
      Объяснение habr.com/ru/company/globalsign/blog/481318/#comment_21049044