스키마 튜토리얼
스키마 튜토리얼레슨 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}"
    }
  })
}

아래는 이 쿼리의 작동 방식에 대한 설명입니다.

자격 증명이 유출될 수 있는 상황

외부 서비스에 연결할 때 자격 증명을 제공해야 하는 경우가 자주 있습니다. 예를 들어, 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 쿼리 내: 자격 증명을 소스 코드에 직접 삽입해서는 안 됩니다. 평문으로 기록되어 보안상 위험이 발생합니다
  • GraphQL 응답 내: 서비스에 연결하는 필드에서 오류가 발생하면 GraphQL 응답의 errors 항목에 오류 메시지가 추가됩니다. 이 메시지에는 실패한 필드 이름과 인수가 포함될 수 있으므로 자격 증명이 출력될 가능성이 있습니다
  • 서버 로그 내: 자격 증명이 변수를 통해 액세스되고 해당 변수가 URL 파라미터로 제공되면 웹 서버의 로그에 기록될 수 있습니다

자격 증명 유출을 방지하는 GraphQL 쿼리

이 GraphQL 쿼리는 자격 증명을 유출하지 않고 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"이며 그 값이 아닙니다

마지막 항목을 설명하기 위해, 존재하지 않는 저장소 "leoloso/NonExisting"의 URL을 GitHub API에 전달하면 오류가 발생하고 다음과 같은 응답이 반환됩니다(오류 메시지의 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
  }
}