Учебник по схеме
Учебник по схемеУрок 21: Как не допустить утечки учётных данных при подключении к сервисам

Урок 21: Как не допустить утечки учётных данных при подключении к сервисам

Этот GraphQL-запрос получает учётные данные из переменной окружения и не допускает их попадания в ответ или логи, тем самым исключая риски безопасности:

query {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @remove
 
  _sendJSONObjectItemHTTPRequest(input:{
    url: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL",
    method: PATCH,
    options: {
      auth: {
        password: $__githubAccessToken
      },
      body: "{\"has_wiki\":false}"
    }
  })
}

Ниже приводится объяснение того, как работает этот запрос.

Как могут произойти утечки учётных данных

При подключении к внешним сервисам нередко требуется передавать учётные данные. Например, REST API GitHub требует токен доступа для конечных точек, где данные являются приватными или изменяются:

query {
  _sendJSONObjectItemHTTPRequest(input:{
    url: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL",
    method: PATCH,
    options: {
      auth: {
        password: "{ GITHUB_ACCESS_TOKEN }"
      },
      body: "{\"has_wiki\":false}"
    }
  })
}

Необходимо проявлять осторожность и не допускать раскрытия учётных данных:

  • В GraphQL-запросе: учётные данные никогда не должны быть встроены в исходный код, поскольку они будут представлены в открытом виде, что создаёт угрозу безопасности
  • В GraphQL-ответе: если поле, обращающееся к сервису, вызывает ошибку, в GraphQL-ответе в секции errors появится сообщение об ошибке; это сообщение может содержать название упавшего поля вместе с его аргументами, а значит — и учётные данные
  • В логах сервера: если учётные данные передаются через переменную, а эта переменная указывается как параметр URL, она может быть записана в логах веб-сервера

GraphQL-запрос, исключающий утечку учётных данных

Этот GraphQL-запрос передаёт учётные данные в API GitHub, не допуская их утечки:

query {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @remove
 
  _sendJSONObjectItemHTTPRequest(input:{
    url: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL",
    method: PATCH,
    options: {
      auth: {
        password: $__githubAccessToken
      },
      body: "{\"has_wiki\":false}"
    }
  })
}

Это достигается по следующим причинам:

  • Учётные данные получаются из переменной окружения GITHUB_ACCESS_TOKEN, поэтому их не нужно встраивать в исходный код
  • Поле githubAccessToken удаляется с помощью @remove, поэтому оно не попадает в ответ
  • Входной параметр _sendJSONObjectItemHTTPRequest(auth:) ссылается на динамическую переменную $__githubAccessToken, поэтому если поле вызовет ошибку, в сообщении об ошибке будет напечатана литеральная строка "$__githubAccessToken" (а не её значение)

Чтобы продемонстрировать последний пункт: если передать в API GitHub URL несуществующего репозитория "leoloso/NonExisting", возникнет ошибка, и мы получим следующий ответ (обратите внимание на auth: {password: $__githubAccessToken} в сообщении об ошибке):

{
  "errors": [
    {
      "message": "Client error: `PATCH https://api.github.com/repos/leoloso/NonExisting` resulted in a `404 Not Found` response:\n{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest/repos/repos#update-a-repository\"}\n",
      "locations": [
        {
          "line": 21,
          "column": 3
        }
      ],
      "extensions": {
        "path": [
          "_sendJSONObjectItemHTTPRequest(input: {url: \"https://api.github.com/repos/leoloso/NonExisting\", method: PATCH, options: {auth: {password: $__githubAccessToken}, body: \"{\"has_wiki\":false}\"}})",
          "query { ... }"
        ],
        "type": "QueryRoot",
        "field": "_sendJSONObjectItemHTTPRequest(input: {url: \"https://api.github.com/repos/leoloso/NonExisting\", method: PATCH, options: {auth: {password: $__githubAccessToken}, body: \"{\"has_wiki\":false}\"}})",
        "id": "root",
        "code": "PoP/ComponentModel@e1"
      }
    }
  ],
  "data": {
    "_sendJSONObjectItemHTTPRequest": null
  }
}