Пентест приложений с GraphQL



    В последнее время GraphQL набирает всё большую популярность, а вместе с ней растёт и интерес со стороны специалистов информационной безопасности. Технологию используют такие компании, как: Facebook, Twitter, PayPal, Github и другие, а это значит, что пора разобраться, как тестировать такое API. В этой статье мы расскажем о принципах этого языка запросов и направлениях тестирования на проникновение приложений с GraphQL.

    Зачем нужно знать GraphQL? Этот язык запросов активно развивается и всё больше компаний находят ему практическое применение. В рамках программ Bug Bounty популярность этого языка тоже растёт, интересные примеры можно посмотреть здесь, здесь и здесь.

    Подготовка

    Тестовая площадка, на которой вы найдете большинство примеров, приведённых в статье.
    Список с приложениями, которые вы тоже можете использовать для изучения.

    Для взаимодействия с различными API лучше использовать IDE для GraphQL:


    Последнее IDE мы и рекомендуем: в Insomnia удобный и простой интерфейс, есть множество настроек и автодополнение полей запросов.

    Перед тем, как перейти непосредственно к общим методам анализа безопасности приложений c GraphQL, вспомним основные понятия.

    Что такое GraphQL?


    GraphQL — язык запросов для API, призванный обеспечить более эффективную, мощную и гибкую альтернативу REST. В его основе лежит декларативная выборка данных, то есть клиент может точно указать, какие именно данные ему нужны от API. Вместо нескольких конечных точек API (REST) GraphQL представляет единую конечную точку, которая предоставляет клиенту запрашиваемые данные.

    Основные различия между REST и GraphQL


    Обычно в REST API вам необходимо получать информацию с разных конечных точек. В GraphQL для получения тех же данных вам необходимо сделать один запрос с указанием данных, которые вы хотите получить.



    REST API предоставляет ту информацию, которую в API заложит разработчик, то есть в случае, если вам необходимо получить больше или меньше информации, чем предполагает API, то нужны будут дополнительные действия. Опять же, GraphQL выдаёт точно запрашиваемую информацию.
    Полезным дополнением будет то, что в GraphQL есть схема, описывающая, как и какие данные клиент может получить.

    Виды запросов


    В GraphQL существует 3 основных вида запросов:

    • Query
    • Mutation
    • Subscription

    Query

    Запросы Query используются для получения/чтения данных в схеме.

    Пример такого запроса:

    query {
      allPersons {
        name
      }
    }

    В запросе указываем, что хотим получить имена всех пользователей. Помимо имени мы можем указать и другие поля: age, id, posts и др. Чтобы узнать, какие именно поля мы можем получить, нужно нажать Ctrl+Пробел. В данном примере мы передаём параметр, с которым приложение вернёт первые две записи:

    query {
      allPersons(first: 2) {
        name
      }
    }

    Mutation

    Если тип query нужен для чтения данных, то тип mutation нужен для записи, удаления и изменения данных в GraphQL.

    Пример такого запроса:

    mutation {
      createPerson(name:"Bob", age: 37) {
        id
        name
        age
      }
    }

    В этом запросе мы создаём пользователя с именем Bob и возрастом 37 (эти параметры передаются как аргументы), во вложении (фигурных скобках) указываем, какие данные мы хотим получить от сервера после создания пользователя. Это нужно для того, чтобы понять, что запрос выполнен успешно, а также для получения данных, которые сервер генерирует самостоятельно, такие как id.

    Subscription

    Ещё один вид запросов в GraphQL — subscription. Он нужен для оповещения пользователей о каких-либо изменениях, произошедших в системе. Работает это так: клиент подписывается на какое-то событие, после чего с сервером устанавливается соединение (обычно через WebSocket), и, когда это событие происходит, сервер отсылает клиенту уведомление по установленному соединению.

    Пример:

    subscription {
      newPerson {
        name
        age
        id
      }
    }

    Когда создастся новый Person, сервер отошлёт клиенту информацию. Наличие запросов subscription в схемах встречается реже, чем query и mutation.

    Стоит отметить, что все возможности по query, mutation и subscription создаёт и настраивает разработчик конкретной API.

    Факультатив


    На практике разработчики часто используют alias и OperationName в запросах для внесения ясности.

    Alias

    GraphQL для запросов предусматривает возможность alias, которая может облегчить понимание того, что именно запрашивает клиент.

    Предположим, что у нас есть запрос вида:

    {
      Person(id: 123) {
        age
      }
    }

    который выведет имя пользователя с id 123. Пусть имя этого пользователя будет Vasya.

    Чтобы в следующий раз не ломать голову, что выведет данный запрос, можно сделать вот так:

    {
      Vasya: Person(id: 123) {
        age
      }
    }

    OperationName

    Помимо alias в GraphQL используется OperationName:

    query gettingAllPersons {
      allPersons {
        name
        age
      }
    }

    OperationName нужен для пояснения того, что именно делает запрос.

    Пентест


    После того, как мы разобрались с основами, переходим непосредственно к пентесту. Как понять, что приложение использует GraphQL? Вот пример запроса, в котором есть GraphQL-запрос:

    POST /simple/v1/cjp70ml3o9tpa0184rtqs8tmu/ HTTP/1.1
    Host: api.graph.cool
    User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0
    Accept: */*
    Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
    Accept-Encoding: gzip, deflate
    Referer: https://api.graph.cool/simple/v1/cjp70ml3o9tpa0184rtqs8tmu/
    content-type: application/json
    Origin: https://api.graph.cool
    Content-Length: 139
    Connection: close
    
    {"operationName":null,"variables":{},"query":"{\n  __schema {\n    mutationType {\n      fields {\n        name\n      }\n    }\n  }\n}\n"}

    Некоторые параметры, по которым можно понять, что перед вами GraphQL, а не что-то иное:

    • в теле запроса имеются слова: __schema, fields, operationName, mutation и др.;
    • в теле запроса много символов "\n". Как показывает практика, их можно убрать, чтобы было удобнее читать запрос;
    • часто путь отправки запроса на сервер: ⁄graphql

    Отлично, нашли и определили. Но куда вставлять кавычку как узнать, с чем нам нужно работать? На помощь придёт интроспекция.

    Интроспекция


    В GraphQL предусмотрена схема интроспекции, т.е. схема с описанием данных, которые мы можем получить. Благодаря этому мы можем узнать, какие запросы существуют, какие аргументы им можно/нужно передавать и многое другое. Заметим, что в отдельных случаях разработчики намеренно не разрешают возможность интроспекции их приложения. Тем не менее, основное большинство всё же оставляет такую возможность.

    Рассмотрим основные примеры запросов.

    Пример 1. Получение всех видов запросов

    query {
      __schema {
        types {
          name
          fields {
            name
          }
        }
      }
    }

    Формируем запрос query, указываем, что хотим получить данные по __schema, а в ней типы, их имена и поля. В GraphQL существуют служебные имена переменных: __schema, __typename, __type.

    В ответе мы получим все типы запросов, их имена и поля, которые существуют в схеме.

    Пример 2. Получение полей для конкретного вида запроса (query, mutation, description)

    query {
      __schema {
        queryType {
          fields {
            name
            args {
              name
            }
          }
        }
      }
    }

    Ответом на данный запрос будут все возможные запросы, которые мы можем выполнить к схеме для получения данных (вид query), и возможные/необходимые аргументы для них. Для некоторых запросов указание аргумента(ов) обязательно. Если выполнить такой запрос без указания обязательного аргумента, сервер должен выдать сообщение с ошибкой, что необходимо его указать. Вместо queryType мы можем подставлять mutationType и subscriptionType для получения всех возможных запросов по мутациям и подпискам соответственно.

    Пример 3. Получение информации о конкретном типе запроса

    query {
      __type(name: "Person") {
        fields {
          name
        }
      }
    }

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

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

    Information disclosure

    Чаще всего приложение, использующее GraphQL, состоит из множества полей и типов запросов, и, как известно многим, чем сложнее и больше приложение, тем сложнее его настраивать и следить за его безопасностью. Именно поэтому при тщательной интроспекции можно найти что-нибудь интересное, например: ФИО пользователей, номера их телефонов и другие критичные данные. Поэтому если вы хотите найти что-то подобное, то рекомендуем проверять все возможные поля и аргументы приложения. Так в рамках пентеста в одном из приложений были обнаружены данные пользователей: ФИО, номер телефона, дата рождения, некоторые данные карт и пр.

    Пример:

    query {
      User(id: 1) {
        name
        birth
        phone
        email
        password
      }
    }
    

    Перебирая значения id, мы сможем получить информацию о других пользователях (а, может, и нет, если всё настроено правильно).

    Injections

    Стоит ли говорить, что практически везде, где есть работа с большим объёмом данных, есть и базы данных? А где есть БД — там могут быть и SQL-injections, NoSQL-injections и другие виды инжектов.

    Пример:

    mutation {
      createPerson(name:"Vasya'--+") {
        name
      }
    }

    Здесь элементарная SQL-инъекция в аргументе запроса.

    Authorization bypass
    Допустим, мы можем создавать пользователей:

    mutation {
      createPerson(username:"Vasya", password: "Qwerty1") {
      }
    }

    Предположив, что есть некий параметр isAdmin в обработчике на сервере, мы можем отправить запрос вида:

    mutation {
      createPerson(username:"Vasya", password: "Qwerty1", isAdmin: True) {
      }
    }

    И сделать пользователя Vasya администратором.

    DoS


    Помимо заявленного удобства в GraphQL есть и свои недочеты с точки зрения безопасности.

    Рассмотрим пример:

    query {
      Person {
        posts {
          author {
            posts {
              author {
                posts {
                  author ...
                }
              }
            }
          }
        }
      }
    }

    Как видите, мы создали зацикленный вложенный запрос. При большом количестве таких вложений, например, в 50 тыс., мы можем отправить запрос, который будет очень долго обрабатываться сервером или вообще «уронит» его. Вместо обработки валидных запросов сервер будет занят распаковкой гигантской вложенности запроса-пустышки.

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

    Вывод


    Итак, мы рассмотрели основные принципы тестирования на проникновение приложений с GraphQL. Надеемся, вы узнали что-то новое и полезное для себя. Если вам интересна данная тема, и вы хотите изучить её глубже, то рекомендуем следующие ресурсы:


    И не забывайте: practice makes perfect. Удачи!
    Digital Security
    240,00
    Безопасность как искусство
    Поделиться публикацией

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

      0
      Очень интересно! Спасибо! Только по вашей статье начал немного понимать что и как работает в GraphQL.
        +2
        Все же GraphQL дырявый или нет? Хотелось бы прочитать более конкретные выводы.

        REST API предоставляет ту информацию, которую в API заложит разработчик, то есть в случае, если вам необходимо получить больше или меньше информации, чем предполагает API, то нужны будут дополнительные действия. Опять же, GraphQL выдаёт точно запрашиваемую информацию.
        Полезным дополнением будет то, что в GraphQL есть схема, описывающая, как и какие данные клиент может получить.

        Для получания данных, не входящих в текущую GraphQL схему, все же надо дергать программистов, так что нельзя сказать, что GraphQL имеет какое-то преимущество перед REST.
          +3
          GraphQL дырявый ровно настолько, насколько это реализует разработчик. Нельзя сказать дырявый REST или нет? Дырявый GraphQL или нет? Это стандарты, которые описывают то, как должно быть устроено API-приложение. А дырявым Вы его сделаете или нет — решать Вам.
            0
            Хотелось бы тогда в выводах услышать некоторые рекомендации по организации сервиса, а то упоминаются возможные проблемы, а вот способов их избежать — нет. Спасибо.
              0
              Целью статьи было написать про пентест, как было указано в названии. Про безопасность GraphQL Вы можете прочитать в конце статьи по ссылке.
          0
          Спасибо за статью! А можно ли как-то понять, откуда конкретно qraphql берет информацию для ответа? Например, что name из запроса — это поле name в таблице tablename?
            +3
            В рамках интроспекции Вы сначала узнаете какие запросы существуют в принципе, затем какие есть поля у каждого запроса можно узнать по примеру 3, что в статье. Чтобы точнее понять, какие поля есть и что из них что означает рекомендую потестировать приложения, указанные в начале статьи. После этого многие вопросы сами решатся.
            0
            mutation {
            createPerson(username:«Vasya», password: «Qwerty1», isAdmin: True) {
            }
            }

            Если в схеме не объявлено поле isAdmin, то GraphQL не примет на его на вход, даже если в самой модели данных куда пишет запрос такое поле существует.
            А если же разработчик объявил в клиентской съеме такое поле, то это большой вопрос, доверять ли разработку АПИ этому человеку.
              0
              Согласен, однако наш мир не идеален и человеческий фактор почти всегда присутствует. Пентест отчасти и нужен для того, чтобы таких ошибок было как можно меньше, ведь даже крупные компании могут допускать такие промахи:
              Github authorization bypass vulnerability
              0
              В тексте допущена ошибка?
              В GraphQL существует 3 основных вида запросов:

              • Query
              • Mutation
              • Description



              Вместо Subscription указано Description?
                0
                Исправил, спасибо.

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

              Самое читаемое