07-security-governance — 보안 & 거버넌스
이 챕터가 답하는 질문: GraphQL의 공격 면적은 무엇이고, 큰 조직은 그래프를 어떻게 관리하는가? 한 줄 답 (Pyramid Top): 공격 면적이 endpoint가 아니라 쿼리 자체이므로 depth·complexity·introspection·auth를 서버에서 강제해야 하고, 큰 그래프는 버전이 아니라 진화로 운영한다.
한 문장 답 (Pyramid Top)
REST는 endpoint 단위로 인증·rate limit·로깅을 끼울 수 있다 — path가 “이 사람이 뭘 하는지”를 알려준다. GraphQL은 endpoint가 하나이고, 그 안에 보내진 쿼리 한 줄이 전체 그래프의 어디든 탐색할 수 있다. 즉 GraphQL의 공격 면적은 쿼리 자체다 — depth, complexity, alias, introspection, batching이 모두 서버에서 강제돼야 한다. 한편 큰 조직의 거버넌스 문제는 “필드를 어떻게 늘리고 어떻게 줄이느냐” 인데, GraphQL은 버전이 없다 — 대신
@deprecated로 진화의 책임을 빌더에게 옮긴다. 이 챕터는 공격 면적(01)에서 시작해, depth/complexity(02), introspection(03), auth(04), rate limit & cost(05), linting & governance(06), versioning-없음(07)까지 7층으로 보안과 거버넌스를 분해한다.
챕터 지도 (Mermaid)
Why — 왜 이 챕터를 별도로 빼는가
GraphQL은 디폴트로 안전하지 않다. spec은 “쿼리를 검증하고 실행하라”고 적혀 있지만, 어떤 쿼리를 거절할 것인가는 한 줄도 적혀 있지 않다. 그래서 production에서 다음 5가지가 직접 손을 봐야 한다.
| 위험 | REST에서는 | GraphQL에서는 | 어디서 다루나 |
|---|---|---|---|
| 거대 응답 | path별 응답 크기 알려져 있음 | { user { posts { author { posts { ... } } } } } 한 줄로 폭발 | 01, 02 |
| 스키마 노출 | OpenAPI spec을 명시 공개 | introspection이 디폴트 켜짐 — 공격자가 schema를 통째로 얻음 | 03 |
| 권한 | URL path로 분리 가능 | endpoint 하나 — 필드 단위 권한 필요 | 04 |
| Rate limit | req/sec 기반 충분 | 쿼리당 복잡도가 다름 — cost 기반 필요 | 05 |
| 그래프 진화 | v1/v2/v3 endpoint 추가 | 한 그래프 — 필드 추가/폐기로 진화 | 06, 07 |
여기서 핵심은 공격 면적의 위치 이동이다. REST에서는 nginx 한 줄(limit_req_zone)로 끝나던 일이, GraphQL에서는 쿼리 파서가 끝난 직후, resolver가 실행되기 전의 좁은 창에서 모두 처리돼야 한다.
How — 어떻게 읽나
다음 7개 문서를 순서대로 읽으면 약 90분이 걸린다. 각 문서는 독립적으로 읽혀도 되지만, 누적적이다.
| # | 파일 | 읽는 데 | 핵심 키워드 |
|---|---|---|---|
| 01 | 01-attack-surface-of-graphql.mdx | 12분 | introspection · alias · batching · cyclic · OWASP cheat sheet |
| 02 | 02-query-depth-and-complexity-limit.mdx | 15분 | depth · complexity · cost · GitHub v4 · Shopify · graphql-query-complexity |
| 03 | 03-introspection-control.mdx | 12분 | NoIntrospection · persisted query · schema registry · trade-off |
| 04 | 04-authentication-and-authorization.mdx | 14분 | context · @auth directive · graphql-shield · Apollo · casbin · oso |
| 05 | 05-rate-limiting-and-cost-analysis.mdx | 14분 | token bucket · cost point · per-field · GitHub 5000/h |
| 06 | 06-schema-linting-and-governance.mdx | 12분 | graphql-eslint · Apollo Rover · 단수/복수 · deprecation reason |
| 07 | 07-versioning-non.mdx | 10분 | @deprecated · 추가는 안전 · 사용량 분석 · 진화 책임 |
의존성: 02는 01을, 03은 01을, 04는 01을, 05는 02를, 06~07은 04까지 가정한다.
What — 한 페이지 요약 (모든 문서의 핵심 한 줄)
| 문서 | 한 줄 결론 |
|---|---|
| 01 | 공격 면적은 endpoint가 아니라 쿼리 자체 — introspection·alias·batching·cyclic 4가지가 4대 공격 패턴이다. |
| 02 | depth limit < complexity limit < cost-based — 정밀도와 구현 비용의 trade-off이며, 셋 다 서버에서 강제돼야 한다. |
| 03 | production introspection 끄기 vs 켜기는 정책 결정이다 — persisted query를 쓰면 introspection이 필요 없어진다. |
| 04 | 인증은 context에 user 주입, 권한은 resolver 단위 또는 @auth directive — directive는 schema에 보이고, middleware는 코드에 숨는다. |
| 05 | REST의 token bucket은 그대로 못 쓰고, 쿼리당 cost로 환산해야 한다 — GitHub v4의 5000 point/h가 사실상 표준 예시. |
| 06 | 큰 그래프는 컨벤션으로 산다 — 단수/복수, ID! vs Int, mutation 명명, deprecation reason은 graphql-eslint·rover로 강제. |
| 07 | GraphQL은 버전이 없다 — @deprecated로 진화하며, 필드 추가는 항상 안전·제거는 사용량 분석 후. |
What-if — 이 챕터를 건너뛰면
01(공격 면적)을 모르면: nginx로만 막아 두고 안심하다가query { __schema { types { fields { type { fields { ... } } } } } }한 줄로 DB가 죽는다.02(depth/complexity)를 안 깔면: 악의 없는 클라이언트가 재귀 쿼리 한 번에 timeout을 부른다 — 사용자는 “GraphQL 느려요”라고만 말한다.03(introspection)을 모르면: production에 introspection을 켠 채 배포해서 공격자가 schema 전체를 합법적으로 다운로드해 간다.04(auth)를 모르면: middleware 한 줄에if (!ctx.user) throw만 박아 두고 — 내부 필드까지 권한 검사 없이 노출된다.05(rate limit)을 모르면: REST식100 req/min만 켜 두고 — 한 요청 안의 100개 필드에 당한다.06(governance)을 모르면: 팀이 늘면getUser/findUser/userById가 같은 그래프에 공존하기 시작한다.07(versioning)을 모르면: REST 습관으로/v2/graphql을 만들고 — 그 순간 GraphQL의 살아 있는 그래프 모델이 깨진다.
Insight — 한 단락 이야기
“GraphQL은 spec이 정의하지 않은 곳에서 무너진다”
2018년 Black Hat USA에서 발표된 GraphQL 보안 세션의 첫 슬라이드는 이랬다 — “우리는 모든 공격을 spec 안에서 정당하게 했다.” 그들이 보여준 공격(introspection 덤프, alias amplification, cyclic query, batch flooding)은 어느 것도 spec 위반이 아니었다. 모두 합법적인 쿼리였다. 그 의미는 분명하다 — spec이 정의하지 않은 곳이 보안의 위치다. depth limit도 complexity limit도 introspection 끄기도 spec에 없다. spec은 “쿼리가 들어오면 검증하고 실행하라”고만 적혀 있고, *“검증의 한계는 네가 정해라”*고 말한다. 추상화가 자유로울수록, 그 자유를 서버 쪽에서 닫는 책임이 무겁다 — 이 챕터가 하는 일은 그 닫기를 7층으로 분해하는 것.
Mermaid 4색 규약
공격 면적 다이어그램
5개의 공격 패턴(danger)이 모두 endpoint 하나로 들어오고, Validator → AuthZ → Rate limit의 세 관문(result)을 통과해야 resolver에 닿는다. 이 세 관문이 spec 외부의 정책이라는 점이 이 챕터의 출발점이다.
한 단락 요약
GraphQL의 공격 면적은 쿼리 자체이며(
01), 그 면적을 서버에서 닫는 도구가 depth/complexity(02)와 introspection 제어(03)다. 다음 층은 권한(04)과 cost 기반 rate limit(05)으로, 둘 다 필드 단위가 기본 단위가 된다. 마지막은 거버넌스 — 컨벤션과 linting(06)으로 큰 그래프를 컨벤션으로 운영하고, 버전 없이 진화(07)하는 정책으로 마감한다. 이 챕터를 끝내면 “GraphQL은 안전한가요?” 라는 질문 대신 “이 쿼리의 cost는 얼마이고, 누가 어느 필드에 권한이 있는가?” 라는 질문을 던지게 된다. 다음 챕터(08-theory-and-alternatives)는 이 모든 결정의 이론적 배경과 대안 접근들을 다룬다.