본문 바로가기
2. 우당탕탕 개발자/2-2. 상세 노트

기술 발표) GraphQL 보안 위협과 지키는 방법

by Little Monkey 2020. 7. 20.
반응형

악성 Query 로 부터 당신의 소중한 GraphQL App을 지키는 방법

 

GraphQL gives enormous power to clients. But with great power come great responsibilities 

그래프큐엘은 클라이언트에게 막대한 권한을 주는 만큼, 클라이언트로 인한 여러 보안 상의 위협을 받기도 합니다. 우리의 작고 소중한 앱을 위협하는 악성 쿼리 중 Ddos 공격에 대해서 해결책 중심으로 살펴보겠습니다. (feat. 개구리앱)

 

공격!

:값비싸고 중첩된 쿼리로 너의 작고 소중한 앱 서버를 파괴할 거야!

 

 

 

 

지금 보고 계시는 데이터 관계도는 개구리의 관계도입니다. 서비스가 복잡하면 할수록 데이터 테이블 간의 관계가 복잡해지고 서로가 서로를 참조하는 관계가 얼마든지 발생할 수 있습니다. 

 

예를 들어 개구리 앱에서 프로젝트 초대가 가능한 유저의 리스트를 얻는 쿼리를 살펴볼게요!

GetInvitableUserList > [User] > [Project]>[projectPositionNo]>[ProjectCandidate]>candidate: User>.....반복

 

물론 클라이언트를 올바르게 사용할 경우, 저렇게 깊게 들어갈 일도 없고 candidate는 다른 type으로 설정되어 있긴 하지만요. 실험을 위해 candidate를 User 타입으로 수정하고, 무한 반복이 가능한 그림을 그려보겠습니다. 이 경우, 마음만 먹으면 다음과 같은 끔찍한 쿼리 공격을 할 수 있습니다. 물론 수많은 반복외에도 많은 쿼리 field를 한 번에 요청하여 서버를 다양한 방법으로 망가뜨릴 수 있습니다. 

 

제 오래된 컴퓨터는 이 정도 공격에도 뜨거워지며 렉에 걸린 듯 멈춰버렸습니다. 만약 당신의 소중한 앱에 이런 무자비한 쿼리 공격이 지속적으로 가해진다면, 서버를 배포한 사람의 예산은 한 없이 늘어나고 서버는 망가질 것입니다. 당신의 소중한 app을 그래프큐엘의 Ddos 공격으로부터 지키기 위해선 어떤 방법이 있을까요? 각각 방법을 소개하고 장점과 단점을 함께 살펴보겠습니다.

01. Timeout (출처)

현재 graphql-jsexpress-graphql 에선 지원하고 있는 timeout option 이 없습니다. 참고로 클라이언트에서 사용할 수 있는 apollo-link-timeout 라이브러리가 최근에 추가되었다고 합니다. 다시 하단의 예제로 돌아와서 config.graphql.queryTimeout에 적합한 시간을 넣어주고, 일정 시간 동안 얼마나 에러를 냈는지 확인 해볼 수 있습니다. 지나치게 짧게 설정해서 클라이언트가 데이터 요청을 다 거절당하는 것은 좋지 않은 방법입니다.

여기서 유추해볼 수 있듯이, 어떤 시간을 설정하는 것이 적합한지를 판단하는 일은 어렵게 느껴집니다. 타임 아웃이 에러를 내기도 전에 악성 코드가 이미 당신의 서버를 파괴할 수 있다는 가능성도 무시할 수 없습니다 .

 


02. query의 총 길이를 제한하는 방법

아래의 구현 예시를 보면, query 의 총길이가 2천자가 넘을 경우 에러를 발생시키는 미들웨어를 붙여주는 방법입니다.

query 의 field 이름이 긴 경우 유효한 query임에도 거절되는 일이 있고, 어디까지를 유효한 쿼리의 길이로 볼 것인가도 정할 것인지도 애매한 문제라 잘 쓰이지 않습니다. 

 


03. Query Whitelist 방법

Cors의 whitelist 처럼, 유효한 query를 whitelist에 담아두고, 그 외엔 어떠한 쿼리도 통과하지 못하게 하는 방법입니다.

그러나 클라이언트에 가능한 많은 권한을 주려고 하는 GraphQL과 white 리스트는 어울리지 않지 않습니다. 서비스의 규모가 커질 수록 클라이언트가 요청할 쿼리를 다 예측하기 어렵고, 매 번 리스트에 없는 쿼리를 요청할 때마다 서버에 요청을 해야 하는 불필요한 일이 따릅니다. 이 방법도 최근에 잘 사용하지 않는 방법입니다.


04. depth limiting 깊이를 제한하는 방법

쿼리의 depth 를 제한하는 방법입니다. Andrew Carlson이 만든 graphql-depth-limit module 를 이용하면 간단하게 쿼리의 깊이를 제한 할 수있습니다. 요청하는 쿼리의 깊이를 계산하여, 일정 깊이를 초과할 경우 에러를 발생하는 모듈입니다. 저는 실험을 위해서 8개의 깊이를 넘을 경우 에러를 발생시키도록 하겠습니다. 

 

(참고로, GraphQL-Yoga가 적용된 개구리 앱에는 `graphql-depth-limit`에 소개되어 있는 예제처럼 GraphqlServer의 옵션에 직접 넣을 수 없어서 어디에 넣어야 할지 한 참 고민했는데, 이렇게 appOtions에 넣을 수 있습니다)

 

만약 다음과 같은 커리를 작성했다고 가정해볼게요.

 

요청하는 쿼리는 깊이가 12로, 8보다 깊이가 깊은 쿼리요청이기 때문에 사진과 같이 에러가 발생했습니다. 

 

모듈만 설치하면 누구나 간단하게 적용할 수 있고, 클라이언트에서 가장 깊은 깊이를 계산하기도 어렵지 않기 때문에 가장 많이 사용되는 방법입니다. 그러나 how to Graphql이라는 사이트에선 해당 방법만으로는 악성 쿼리 공격을 완전히 막기는 어려울 것이라는 의견이 있었습니다. depth 말고, 많은 field를 요청할 경우 depth 공격과 같은 공격의 효과를 줄 수 있다는 이유에서였습니다.

 


05.Query Complexity

쿼리의 복잡성의 한계를 설정해주는 방법입니다. 쿼리의 복잡성이라고 하니 어려운 말같지만, 한 번에 요청하는 쿼리 field의 개수를 생각하면 쉽습니다. 위의 쿼리 뎁스를 조절하는 방법과 같이 appOptions에 다음과 같이 설정할 수 있습니다. 저는 역시나 실험을 위해 10으로 맥시멈을 설정했습니다. 

아까와 같은 옵션에 이렇게 맥시멈을 설정해주고, 아까와 동일한 예제를 이용합니다.

아까와 다르게 깊이가 아니라, 얼마나 많은 field를 서버에 요청하는지가 계산되어 나옵니다.

 

저는 맥시멈을 10으로 설정했기 때문에, 13 복잡성을 가진 해당 요청 쿼리는 에러를 발생했습니다.

 

그러나 이 방법 역시 완벽하게 graphql 의 보안을 책임지는 방법은 아닙니다. 그리고 mutation 의 경우 해당 복잡성을 추정하기가 어렵다는 단점이 존재합니다.

 

결론

 

graphql의 그래프식으로 파고 들어갈 수 있는 관계형 데이터 형식과 클라이언트에 많은 파워를 주는 특징이 이러한 보안 위협의 가능성을 만드는 것 같습니다. 방어 방법들을 살펴보면서, 이렇게 다 장/단점이 있는데, 뭘 어떤 걸 써야 되는거야? 라는 생각이 드실 수 있습니다. 각자의 앱의 환경이 다르기 때문에 어떤것이 특별히 좋다/나쁘다 말할 건 아닌것 같습니다. 

 

다만, 아폴로 블로그에선 complexity에 기반한 throttle을 이용한 방법이 현재로선 최선의 방법이라고 하는 것 같습니다. 대부분의 그래프큐엘을 이용한 API는 클라이언트가 잦은 데이터 요청을 하는 것을 제한하기 위해 throttle을 사용한다고 합니다. Throttle은 일정 기간동안 실행된 기능을 최대 한 번만 실행할 수 있도록 모아서 처리하는 방식입니다. 지금 하단의 gif 파일을 보면 바쁘게 움직이는 개구리 서버를 볼 수 있습니다. 

불과 몇초에 불과한 시간동안 클라이언트가 서버에 요청하는 데이터 모습입니다. 

 

매초 마다 이루어지는 저 요청이 클라이언트에서 매 번 필요로 하는 쿼리는 아닐 것입니다. 불필요하게 요청되고 있는 쿼리들일 가능성도 높다는 것이죠. 이처럼 불필요하게 반복되는 건전한 클라이언트의 요청을 일정 주기로 제한하고, 더불어 나쁜 의도를 가진 채 접근한 나쁜 클라이언트의 악성 쿼리도 throttle을 이용해서 어느 정도 방어 가능합니다 .

 

100초당 요청을 모아서 한 번에 실행시키는 Throttle을 구현했다고 가정할게요. 이를 모르는 클라이언트가 아까의 지독한 쿼리를 반복해 보내도, 서버는 100초 안에 같은 쿼리를 한 번 이상 실행하지 않습니다. 물론 악성 쿼리를 보내는 클라이언트가 다채롭게 보낼 경우는 다채롭게 실행되겠지만 같은 쿼리를 반복하면, 우리의 throttling을 적용한 서버는 100초 안에 한 번만 처리할 것입니다. 여기에 complexity 제한에 걸릴 경우, 그 마저도 실행되지 않겠죠.

 

추가로 여기에 대다수의 배포 사이트에선 ddos공격을 막기 위해 다양한 설정을 준비해두었습니다. 개구리가 배포되어 있는 무료 google cloud는 API의 비율 제한을 사용하여 클라이언트의 요청 한도를 제한 할 수 있습니다. 이렇게 하면 클라이언트가 서버를 무제한으로 사용하는 것을 방지할 수 있습니다. 기본적으로 구글에서 제공하는 API 제한 한도는 다음과 같습니다.

각 제한 카테고리는 별도로 계산되므로 각 카테고리에서 동시에 최대 제한에 도달할 수 있습니다. 비율 제한은 100초 간격으로 적용됩니다. 예를 들어 초당 20개 요청은 100초 내 2,000개 요청으로 변환됩니다. 즉, 100초 이내에 지정된 제한에 도달하면 기다렸다가 할당량 버킷이 새로고침된 후에야 추가 요청을 실행할 수 있습니다. (구글 페이지)

 

100초에 한 번씩 2000개의 요청을 처리합니다. 100초 이내에 2001개를 요청할 경우, 1개의 요청은 2000개가 다 처리된 이후에 실행할 수 있습니다. 마치 위에서 살펴본 throttling 과 비슷하지 않나요? 사용하고 있는 클라이언트가 리소스 사용량이 많은 경우 초당 10개 정도로 조정이 가능합니다. 이 역시 사용자가 구글 플랜 하에서 커스터마이징이 가능합니다. 

 

 

 

이처럼 Throttle 과 complexity 를 이용해서 악성 쿼리를 잡아 내는 최선의 방법처럼 보여집니다. 그런데, 제가 개구리에 취한 보안 방법은  대다수 블로그에서 소개하고 있는 depth-limit방법입니다. 이유는 구현하기 쉽고, 눈에 띄는 공격을 방어할 수 있기 때문입니다. 구글에도 최소한의 Ddos 공격에 대처하고 있기 때문에, 저희의 작고 소중한 개구리는 depth-limit 정도로도 충분히 방어 될 것이라는게 제 개인적인 생각입니다.

 

 

참고자료:

https://www.howtographql.com/advanced/4-security/

https://medium.com/swlh/protecting-your-graphql-api-from-security-vulnerabilities-e8afdfa6fbe4

https://www.apollographql.com/blog/securing-your-graphql-api-from-malicious-queries-16130a324a6b/

https://cloud.google.com/compute/docs/api-rate-limits

반응형

댓글