How to Securing GraphQL

GraphQL은 클라이언트가 필요한 데이터만 정확하게 요청할 수 있도록 허용하여 기존 REST API에 비해 뛰어난 유연성과 효율성을 제공합니다. 하지만 이러한 유연성은 고유한 보안 문제를 야기합니다. 이를 해결하지 못하면 데이터 유출, 서비스 거부(Denial of Service), 권한 상승 취약점으로 이어질 수 있습니다.

이 글에서는 GraphQL과 관련된 주요 보안 위협을 설명하고, GraphQL 기반 애플리케이션을 보호하기 위한 실용적인 전략을 제공합니다. 핵심 원칙은 네트워크 경계뿐만 아니라 애플리케이션 로직 깊숙한 곳에서도 보안 통제를 구현하는 것입니다.

The GraphQL Request Lifecycle

안전한 GraphQL 요청 처리 흐름은 여러 계층의 유효성 검사와 확인을 포함합니다.

 graph TD
    A[Client Request] --> B{HTTP Middleware};
    B --> C{Authentication};
    C -- Authenticated --> D[Parse Query];
    C -- Failed --> Z[Reject Request];
    D --> E{Validation};
    E -- Cost/Depth OK --> F[Execute Resolvers];
    E -- Invalid Query --> Z;
    F -- For each field --> G{Authorization Check};
    G -- Authorized --> H[Fetch Data];
    G -- Unauthorized --> I[Return Null/Error];
    H --> J[Format Response];
    I --> J;
    J --> K[Return to Client];

    subgraph "Pre-Execution"
        B
        C
        D
        E
    end

    subgraph "Execution"
        F
        G
        H
        I
    end

Abusing Introspection

  • Problem: Introspection은 클라이언트가 GraphQL 스키마 자체를 쿼리하여 모든 타입, 필드, 쿼리, 뮤테이션을 노출시킬 수 있습니다. 개발 도구에는 유용하지만, 공격자에게는 API의 전체 구조를 알려주는 지도가 됩니다.
  • Mitigation: 프로덕션 환경에서는 Introspection을 비활성화해야 합니다. 이는 일반적으로 사용 중인 GraphQL 서버 라이브러리에서 설정 플래그로 제공됩니다. 예를 들어, apollo-server에서는 서버를 인스턴스화할 때 다음과 같이 설정할 수 있습니다.
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production'
});

Denial of Service (DoS)

  • Problem: 공격자는 과도한 서버 리소스를 소모하는 깊게 중첩되거나 복잡한 쿼리를 작성하여 정상적인 사용자에 대한 서비스 거부를 유발할 수 있습니다.
  • Mitigations:
    • Query Depth Limiting: 쿼리의 최대 중첩 수준을 제한합니다. 예를 들어, 10단계 이상 중첩된 쿼리는 거부합니다.
    • Query Cost Analysis: 각 필드의 계산 복잡성에 따라 숫자 "비용"을 할당합니다. 쿼리를 실행하기 전에 총비용을 계산하고 미리 정의된 임계값을 초과하면 거부합니다.
    • Timeouts: 장시간 실행되는 쿼리가 서버 리소스를 무기한으로 점유하는 것을 방지하기 위해 쿼리 실행에 대한 강제 시간 초과를 설정합니다.
    • Amount Limiting (Pagination): 리스트에서 반환될 수 있는 레코드 수를 항상 제한해야 합니다. 클라이언트가 무제한의 항목을 요청하도록 허용해서는 안 됩니다.

Authorization Flaws

  • Problem: GraphQL 리졸버는 특정 필드의 데이터를 가져옵니다. 만약 각 객체에 대한 리졸버 레벨에서 권한 검사가 수행되지 않으면, 공격자는 자신이 볼 권한이 없는 데이터에 접근할 수 있습니다. 이는 대표적인 Insecure Direct Object Reference (IDOR) 취약점 경로입니다.
  • Mitigation: 각 관련 리졸버 내에서 또는 미들웨어 계층으로 권한 검사를 구현해야 합니다. 요청된 모든 데이터 조각에 대해, 애플리케이션은 현재 인증된 사용자가 해당 데이터를 보거나 수정할 권한이 있는지 확인해야 합니다. 엔드포인트 수준에서 인증만 확인하는 것은 충분하지 않습니다.
const resolvers = {
  Query: {
    user: (parent, { id }, context) => {
      // 권한 검사: 로그인한 사용자가 이 프로필을 볼 권한이 있는가?
      if (context.user.id !== id && !context.user.isAdmin) {
        throw new Error('이 사용자를 볼 권한이 없습니다.');
      }
      return db.users.find({ id: id });
    }
  }
};

Insufficient Error Handling

  • Problem: 스택 트레이스나 데이터베이스 오류와 같은 상세한 오류 메시지는 백엔드 인프라, 사용된 라이브러리, 데이터베이스 스키마에 대한 민감한 정보를 유출할 수 있습니다.
  • Mitigation: 모든 예외를 포착하는 전역 오류 핸들러를 구현해야 합니다. 내부 디버깅 목적으로는 상세한 오류를 기록하되, 클라이언트에게는 정제된 일반 오류 메시지를 반환해야 합니다. graphql-errors와 같은 라이브러리는 이 프로세스를 공식화하는 데 도움이 될 수 있습니다.

Authentication

  • Problem: GraphQL은 전송 계층에 독립적이며 특정 인증 메커니즘을 강제하지 않습니다. 이를 올바르게 구현하는 것은 개발자의 책임입니다.
  • Mitigation: GraphQL 엔드포인트를 다른 민감한 API 엔드포인트와 동일하게 취급해야 합니다. OAuth 2.0 또는 JWT와 같은 표준 인증 메커니즘을 구현해야 합니다. 토큰은 Authorization HTTP 헤더로 전달되어야 하며, 쿼리가 처리되기 전에 미들웨어 계층에서 유효성을 검사해야 합니다. 검증된 사용자 정보는 리졸버에서 사용할 수 있도록 GraphQL context 객체에 저장되어야 합니다.

Conclusion

GraphQL API를 보호하는 것은 기존의 엔드포인트 기반 보안 모델에서 벗어나는 사고의 전환이 필요합니다. GraphQL의 유연한 특성은 보안이 애플리케이션 핵심 로직의 필수적인 부분이어야 함을 의미합니다.

프로덕션 환경에서 Introspection을 비활성화하고, 리소스 고갈 공격에 대한 강력한 통제 수단을 구현하며, 리졸버 내에서 객체 수준의 권한 부여를 강제하고, 오류 응답을 신중하게 관리함으로써 개발자는 강력하고 안전한 GraphQL 애플리케이션을 구축할 수 있습니다. 보안은 마지막에 추가하는 기능이 아니라 개발 수명 주기 전반에 걸쳐 고려해야 할 기본적인 요구 사항입니다.