Schema 教程
Schema 教程第21课:连接服务时避免泄露凭据

第21课:连接服务时避免泄露凭据

此 GraphQL query 从环境变量中获取凭据,并避免其被打印到响应或日志中,从而规避安全风险:

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}"
    }
  })
}

以下是对此 query 工作原理的说明。

凭据可能泄露的情形

连接外部服务时,我们通常需要提供凭据。例如,GitHub 的 REST API 在数据私有或需要修改的端点上要求提供访问令牌:

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

我们需要谨慎,避免暴露凭据:

  • 在 GraphQL query 中: 凭据绝不能嵌入源代码,因为其会以明文形式存在,造成安全隐患
  • 在 GraphQL 响应中: 如果连接服务的字段产生错误,错误消息将以 errors 条目的形式添加到 GraphQL 响应中;该消息可能会打印失败字段的名称及其参数,从而将凭据暴露出去
  • 在服务器日志中: 如果凭据通过变量访问,且该变量作为 URL 参数传递,则可能被记录在 Web 服务器的日志中

避免泄露凭据的 GraphQL query

此 GraphQL query 在将凭据传递给 GitHub API 的同时,避免了凭据泄露:

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",而非其实际值

为验证最后一点,向 GitHub API 提供不存在的仓库 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
  }
}