В былые времена, когда веб разработка строилась на том, что серверные приложения направляли запросы в реляционные базы данных и выдавали на выходе HTML, часто встречался такой код:

// ВНИМАНИЕ: Плохой пример!
function popup(msg: string): string {
    return "<p class=\"popup\">" + msg + "</p>";
}

или такой:

// ВНИМАНИЕ: Плохой пример!
function getName(login: string): string {
    return "SELECT name FROM users WHERE login = \"" + login + "\"";
}

С тех пор мы научились использовать более безопасные подходы.

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

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



Чтобы понять почему, разобьем один из неудачных примеров на более простые фрагменты. Вот так:

function f(userInput: A): A {
    const firstCommand: A = ...;
    const secondCommand: A = ...;
    return firstCommand.concat(userInput.concat(secondCommand));
}

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

Естественно, как я уже говорил, существуют хорошо известные средства защиты от таких атак. Вместо вот этого:

"SELECT name FROM users WHERE login = \"" + login + "\""

лучше написать что-нибудь такое:

query("SELECT name FROM users WHERE login = :login", {login})

Таким образом команда SELECT name FROM users WHERE login =:login отчетливо отделяется от данных {login}. В то же время внутренние механизмы гарантируют, что данные будут подготовлены для использования в запросе SQL. Экранировать кавычки и внедрить вредоносный код не получится.

Однако разработка веб-приложений стремительно развивается. Все чаще встречается не только такое:

{
  paramA: "the value of the A parameter",
  paramB: "the value of the A parameter",
}

но и такое:

{
  paramA: "the value of the A parameter",
  paramB: {$in: [
    "the value of the B parameter",
    "the value of the C parameter",
  ]},
}

Существенным отличием между двумя вариантами является то, что значение параметра представляет собой объект, включающий в себя команду!

Допустим значения читаются из userInput:

{
  paramA: userInput.paramA,
  paramB: {$in: [
    userInput.paramB[0],
    userInput.paramB[1],
  ]},
}

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

Проблема в том, что значениями параметра могут быть не только простые значения наподобие the value of the A parameter, но и команды, например {$in: ["B", "C"]}. Пользователь может разными способами направить запрос, после дешифровки которого получается объект (форма, JSON или XML), и поэтому код может подвергаться атакам путем инъекций.

Допустим userInput.paramA равно {$empty: false}. Тогда запрос выглядит следующим образом:

{
  paramA: {$empty: false},
  paramB: {$in: [
    userInput.paramB[0],
    userInput.paramB[1],
  ]},
}

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

Чтобы избежать эту проблему, нужно всегда писать команды так, чтобы их нельзя было получить от пользователя. Это можно реализовать в том числе с помощью подхода, применяемого в React и Snabbdom-Signature (это небольшая библиотека для защиты от инъекций в виртуальный DOM), а именно отмечать каждый объект команды Symbol, чтобы его нельзя было отправить по сети.

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


LOOKING.HOUSE — на проекте собрано более 150 точек looking glass в 40 странах. Можно быстро выполнить команды host, ping, traceroute и mtr.


Комментарии (11)


  1. kaljan
    02.09.2018 23:43

    GraphQl — и никаких инъекций :)


  1. FDA847
    03.09.2018 01:17

    Можно ещё хранимые процедуры использовать.


    1. mayorovp
      03.09.2018 09:45
      -2

      А что, их в Монгу уже завезли?


      1. FDA847
        03.09.2018 09:47

        А где про Монгу написано было?


        1. mayorovp
          03.09.2018 09:49
          -1

          Так использован ее синтаксис запросов же.


          1. FDA847
            03.09.2018 09:52

            В этом что-то уникальное есть?

            SELECT name FROM users WHERE login = \"" + login + "\""

            В других СУБД так нельзя что ли?


            1. mayorovp
              03.09.2018 09:54

              Вы привели пример давно решенной проблемы, который был приведен в статье исключительно чтобы налить воды. А реальная проблема — вот эта:


              {
                paramA: userInput.paramA,
                paramB: {$in: [
                  userInput.paramB[0],
                  userInput.paramB[1],
                ]},
              }


              1. mark_ablov
                03.09.2018 11:30

                $in может встречаться много где. Скажем, sequelize — ORM для SQL на node.js имеет подобный синтаксис.


                1. mayorovp
                  03.09.2018 11:57

                  Согласен. Но возражение будет то же самое — в sequelize нет своих хранимок, а если пользоваться хранимками базы — то зачем вообще sequelize?


  1. Lure_of_Chaos
    03.09.2018 12:28

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

    Спасибо, Кэп, это ведь все, что ты хотел сказать?

    На самом деле проблема глубже: нельзя данные делать слишком «умными». А идеально — максимально жестко ограничивать формат.
    К примеру, если мы ждем параметр «цвет» в формате RGB, то нужно проверять, чтобы это была последовательность символов 0-9, A-F из 6 знаков. даже символ # не нужно разрешать, его можно и так подставить. 6 знаков, не 7, не 8…


    1. MikailBag
      04.09.2018 13:24

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