자신의 코드를 리뷰하는 GitHub App 구축하기: 보안 강화(Security Hardening)를 통한 교훈
요약
GitHub App을 활용한 자동화된 코드 리뷰 봇 구축 과정에서 겪은 보안 강화 사례를 다룹니다. 인증 실패 시 요청을 허용하는 'Fail Open' 문제와 Content-Length 우회 등 실제 취약점을 해결하며 보안 엔지니어링의 중요성을 강조합니다.
핵심 포인트
- 인증 서비스 오류 시 요청을 거부하는 'Fail-Closed' 패턴 적용 필수
- 신뢰할 수 없는 입력값(웹훅 페이로드 등)에 대한 철저한 검증 필요
- 보안 공격 표면을 줄이기 위한 인증, 요청 검증, 리소스 제한의 중요성
- 테스트 커버리지를 62%에서 76%로 개선하며 안정성 확보
자동화된 코드 리뷰 봇을 구축하는 동안, 어떻게 27개의 침묵하는 실패(silent failures)를 로그가 남는 실패로 전환하고, 세 개의 실제 공격 표면(attack surfaces)을 차단하며, 테스트 커버리지를 62%에서 76%로 끌어올렸는지에 대해 이야기합니다.
몇 달 전, 저는 서류상으로는 간단해 보이는 것을 만들기 시작했습니다. 바로 Pull Request(PR)를 리뷰하고, 유출된 비밀(secrets)을 스캔하며, 안전한 자동 수정(autofixes)을 적용하고, 팀원처럼 슬래시 명령어(slash commands)에 응답하는 GitHub App입니다. 저는 이를 GitHub Autopilot이라고 불렀습니다. 이 앱은 Flask 위에서 실행되며, 작업 큐잉(job queuing)을 위해 Redis를 사용하고, 웹훅(webhooks)을 통해 GitHub과 통신합니다.
첫 번째 작동 버전이 나오기까지 몇 주가 걸렸습니다. 하지만 타인의 저장소(repository)를 믿고 맡길 수 있을 만큼 안전하게 만드는 데는 훨씬 더 오랜 시간이 걸렸으며, 이는 그 어떤 튜토리얼보다 보안 엔지니어링(security engineering)에 대해 더 많은 것을 가르쳐 주었습니다.
이 글에서는 내부 감사(internal audit) 중에 발견하고 수정했던 세 가지 실제 취약점과, 각 취약점이 왜 중요했는지, 그리고 그 과정이 처음부터 끝까지 어떻게 진행되었는지를 살펴봅니다.
코드 리뷰 봇이 보안 공격 대상이 되는 이유
코드를 읽고, PR에 댓글을 달고, 자동 수정을 푸시할 수 있는 GitHub App은 특권적인 위치에 있습니다. 이 앱은 쓰기 권한(write access)을 가지며, 신뢰할 수 없는 입력값(모든 PR, 모든 웹훅 페이로드)을 처리하고, 종종 다른 서비스와 통신합니다. 저의 경우, AI 지원 리뷰를 위한 MCP (Model Context Protocol) 서버와 통신합니다.
이러한 조합은 세 가지 요소가 완벽해야 함을 의미합니다: 인증(authentication), 요청 검증(request validation), 그리고 리소스 제한(resource limits)입니다. 저는 이 세 가지 모두에 대해 작동하는 코드를 가지고 있었습니다. 하지만 제대로 감사하기 전까지는, 공격 상황에서 각 요소가 안전하게 실패(fail safely)한다는 증거가 없었습니다.
문제 1: 개방형으로 실패하는 인증 (Authentication That Failed Open)
MCP 통합은 외부 서비스로부터의 요청을 처리했습니다. 저의 원래 인증 체크 방식은 다음과 같았습니다: 토큰이 있으면 검증하고, 만약 검증 자체에서 오류가 발생하면(예: 인증 서비스가 느리거나 도달할 수 없는 경우), 코드가 예외(exception)를 포착하고 요청을 그대로 통과시키는 방식이었습니다.
이것을 '페일 오픈(failing open)'이라고 부르며, 기존 코드 경로에 보안을 덧붙이는 시스템에서 발생하는 가장 흔한 실수 중 하나입니다.
의도는 합리적이었습니다. 불안정한 의존성 (flaky dependency) 때문에 앱 전체가 다운되지 않도록 하는 것이었죠. 하지만 결과는 위험했습니다. 인증 서비스 (auth-service)의 타임아웃 (timeout)을 유발할 수 있는 공격자가 인증 과정을 완전히 건너뛸 수 있게 된 것입니다.
해결책은 기본 설정을 뒤집는 것이었습니다. 이제 인증 과정 중 발생하는 모든 예외 (exception)는 통과가 아닌 자동 거부로 이어집니다. 인증 서비스에 접속할 수 없는 경우, 요청은 거부되고 로그가 기록되며 재시도됩니다. 결코 조용히 신뢰되지 않습니다. 이것이 표준적인 페일 클로즈 (fail-closed) 패턴이며, 모든 보안 검사에서 반드시 기본값으로 설정되어야 합니다.
문제 2: Content-Length 우회
웹훅 (Webhook) 페이로드 (payload)에는 Content-Length 헤더가 포함되며, 앱은 이를 사용하여 처리 전 크기 제한을 강제했습니다. 이는 크기가 너무 크거나 악의적인 페이로드에 대한 합리적인 방어책이었습니다. 문제는 여기서 발생했습니다. 해당 검사가 실제 수신된 바이트 (bytes)가 아닌 헤더 값 자체를 신뢰했다는 점입니다.
요청이 작은 Content-Length를 선언하면서 실제로는 훨씬 더 큰 본문 (body)을 스트리밍하면, 크기 검사를 완전히 통과할 수 있습니다. 이는 HTTP 처리에서 알려진 버그 유형이며, 코드가 헤더를 읽고 숫자를 비교하는 식이라
이는 방어적인 기능을 공격 벡터 (Attack Vector)로 변질시킵니다. 즉, 남용을 막기 위해 만든 바로 그 기능이 서비스 중단을 일으키는 도구가 되어버리는 것입니다.
해결책으로 시간 기반의 만료 정책 (Time-based eviction policy)을 추가하여, IP 엔트리가 영원히 쌓이게 두는 대신 이동 창 (Rolling window) 방식에 따라 오래된 엔트리를 삭제하도록 했습니다. 속도 제한기 (Rate limiter)는 단순히 요청 속도뿐만 아니라 자체적인 메모리 사용량도 제한해야 합니다. 이는 모든 수동 테스트에서 기능이 정상적으로 작동할 때 쉽게 간과하기 쉬운 세부 사항입니다.
더 조용한 문제: 27번의 침묵하는 실패
위의 세 가지 버그 중 그 어떤 것도 코드 리뷰만으로는 찾아내기 쉬운 것이 아니었습니다. 이는 더 깊은 문제를 시사했습니다. 코드베이스 내의 27곳에서 예외 (Exception)를 포착하고도 아무런 조치를 취하지 않고 있었습니다. 에러 처리 경로 전반에 걸쳐 반복된 try/except: pass 패턴은 무언가 잘못되었을 때 앱이 로그 기록도, 알림도, 흔적도 없이 침묵 속에서 계속 실행되게 만들었습니다.
이는 작성하기에는 편안한 패턴이지만, 배포하기에는 위험한 패턴입니다. 이 패턴은 가장 중요한 종류의 실패, 즉 당신이 테스트하지 않은 조건 하에서 운영 환경 (Production)에서 단 한 번 발생하는 실패를 은폐합니다.
그 27개의 핸들러(Handler)는 모두 나중에 디버깅할 수 있도록 충분한 컨텍스트(Context)와 함께 실패를 기록하도록 재작성되었습니다. 어떤 작업이 실패했는지, 어떤 입력이 이를 유발했는지, 그리고 원래의 예외가 무엇이었는지를 포함했습니다. 이 중 어떤 것도 무언가 고장 났을 때 앱이 수행하는 동작 자체를 바꾸지는 않았습니다. 다만, 당신이 그 사실을 알 수 있는지 여부를 바꾸었습니다.
증명하기: 테스트와 커버리지 (Coverage)
테스트 없는 수정은 의견에 불과합니다. 위의 각 보안 변경 사항은 원래의 실패 모드를 재현하는 테스트와 함께 배포되었습니다. 위조된 Content-Length, 인증 서비스 (Auth-service) 타임아웃, 지속적인 부하 상황에서의 속도 제한 카운터 등을 테스트하고 새로운 안전한 동작을 확인(Assert)했습니다.
전체 감사 주기 동안 프로젝트의 테스트 커버리지 (Test coverage)는 62%에서 76%로 증가했으며, 전체 테스트 스위트의 834개 테스트 중 654개가 통과했습니다. 커버리지 수치만으로 정확성을 증명할 수는 없지만, 각 취약점에 대한 타겟 테스트와 결합되었을 때, 저는 이 수정 사항들이 단순히 코드가 컴파일되는 수준을 넘어 실제로 작동한다는 확신을 가질 수 있었습니다.
이 프로젝트가 문서화(Documentation)에 대해 내게 가르쳐준 것
버그를 수정하는 것은 작업의 절반에 불과했습니다. 나머지 절반은 미래의 기여자, 감사자(Auditor), 또는 고객이 실제로 사용할 수 있는 방식으로 기록하는 것이었습니다. 즉, 웹훅(Webhook)이 인증(Auth), 큐잉(Queueing), 그리고 처리(Processing) 과정을 거쳐 어떻게 흐르는지 보여주는 아키텍처 다이어그램(Architecture diagram), 각 공격 표면(Attack surface)을 완화 조치(Mitigation)와 매핑한 위협 모델(Threat-model) 테이블, 그리고 단순히 무엇이 바뀌었는지가 아니라 왜 각 수정이 이루어졌는지를 설명하는 변경 이력(Changelog)이 필요했습니다.
마지막 부분이 가장 중요하다는 사실이 밝혀졌습니다. 코드는 시스템이 무엇을 하는지 보여줍니다. 하지만 문서화는 시스템이 무엇에 맞서 방어하고 있는지, 그리고 왜 방어 체계가 그런 모습인지를 보여주는 유일한 곳입니다. 신뢰할 수 없는 입력(Untrusted input)을 처리하는 모든 시스템—대부분의 시스템이 그러합니다—에게 있어, 그 기록은 수정 사항 자체만큼이나 가치가 있습니다.
GitHub Autopilot은 오픈 소스입니다. 전체 아키텍처 문서, 위협 모델 테이블, 그리고 감사 이력은 GitHub에서 확인할 수 있습니다: Shweta-Mishra-ai/github-autopilot.
저는 TechNova World에서 개발자 도구(Developer tools)를 구축하고 보안을 강화하는 것에 대해 글을 씁니다. 시스템을 구축하는 동시에 팀을 위해 이를 명확하게 문서화할 수 있는 전문가를 찾고 계신다면, 함께 이야기해 봅시다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기