오픈 소스 프로젝트의 취약점 분류 과정에서 유용한 간결한 원칙들을 정리한 글.
나는 취미 시간의 일부를 오픈 소스 프로젝트의 취약점 분류에 쓴다. 그 과정에서 나는 온갖 허튼소리1를 보게 되고, 또 걸러내게 된다.
허튼소리는 취약점 분류에만 있는 것이 아니다. 법률가들도 그것을 다룬다. 법률 세계에서는 이에 대처하기 위해 brocards를 쓴다. 이는 법 원칙의 핵심을 담아내는 간결한 격언이다. 어떤 브로카르도 보편적으로 참인 것은 아니지만2, 어떤 주장의 정당성을 빠르게 평가할 수 있는 기준을 제공한다.
취약점 분류에도 그 나름의 브로카르가 있지만, 나는 그것들을 한데 모아 놓은 포괄적인 목록을 어디에서도 찾지 못했다. 이것은 그런 목록을 만들어 보려는 나의 시도다.
Alex Gaynor는 Motion to Dismiss for Failure to State a Vulnerability에서 이 점을 잘 설명한다. 위협 모델이 빠져 있거나, 제시된 위협 모델이 앞뒤가 맞지 않는다면 취약점 보고는 안전하게 기각할 수 있다.
예시는 다음과 같다.
문서화되지 않았거나 뜻밖의 일부 경우에 예외를 발생시키는 Python API에 대한 보고3인데, 공격자가 그 동작을 어떻게 악용해 피해를 일으킬 수 있는지는 설명하지 않는 경우.
로컬 개발자 도구의 멈춤 또는 정지에 대한 보고. 멈춤은 바람직하지 않은 동작이지만, 개발자 도구 맥락에서 그로 인한 피해 가능성은 무시해도 될 정도로 작다. 개발자는 언제든 그냥 프로세스를 종료하면 되기 때문이다.
이와 밀접하게 관련된 것은 심각한 최종 상태를 설명하지만, 그 전제가 되는 공격자 능력이 취약점 자체보다 더 강력한 취약점 보고들이다. 사실상 취약점을 악용하는 공격을 수행하려면, 공격자는 그 취약점이 제공하는 능력과 같거나 그보다 더 강력한 능력을 이미 가지고 있어야 한다.
예시는 다음과 같다.
웹 서비스에서의 콘텐츠 조작에 대한 보고인데, 그 조작이 공격자가 능동적인 meddler in the middle일 때만 가능한 경우. 여기에는 취약점이 없다. 능동적인 MiTM은 완전히 임의의 콘텐츠를 보낼 수 있으므로, 기존 콘텐츠만 조작하는 데 스스로를 제한할 이유가 없기 때문이다.
메모리 손상을 통한 CPython 코드 실행에 대한 보고인데, 그 메모리 손상이 런타임 중 CPython의 객체 내부를 직접 조작함으로써 발생하는 경우다(예를 들어 ctypes 사용). 여기에는 취약점이 없다. 공격자가 그 손상을 일으키기 위해 이미 임의 코드를 실행하고 있기 때문이다.
Raymond Chen은 2006년에 같은 개념을 다루는 글을 썼다. 그것을 공유해 준 Geoffrey Thomas에게 감사한다!
취약점 보고가 어떤 동작이 _발생할 수 있다_고 설명하더라도, 그 동작이 실제 소프트웨어 사용에서는 일어나지 않는다면 그 보고는 안전하게 기각할 수 있다.
예시는 다음과 같다.
예를 들어, 어떤 C 코드베이스에
char *
를 받는 함수가 있고 100바이트보다 긴 문자열에서 버퍼 오버플로를 일으킨다고 하자. 하지만 그 함수에 대한 모든 호출이 정적으로 그 크기를 넘지 않는다고 단언할 수 있는 코드베이스라면, 그 코드는 취약하지 않다.
예를 들어, 어떤 API에 입력 문자열이 유효한 UTF-8이라는 사전조건이 있을 수 있고, 이 사전조건을 위반하는 입력은 통제되지 않은 프로그램 중단을 일으킬 수 있다. 그러나 이런 동작을 발견한 퍼저가 취약점을 찾아낸 것은 아니다. 실제 프로그램에서는 API의 “구성 블록”들이 함께 올바르게 조합되도록 보장할 책임이 프로그래머에게 있기 때문이다.
여기에는 짚고 넘어갈 만한 미묘한 점이 있다. 불변식을 유지할 책임이 프로그래머에게 있기 때문에, API의 사용 이 그 불변식을 위반하는 경우에는 잠재적으로 정당한 취약점이 실제로 존재할 수 있다. 비유하자면:
free(3)
자체는 이중 해제에 취약하다고 보지 않지만, 이미 해제된 포인터에 대해
free(3)
를 호출하는 프로그램은 이중 해제에 취약하다고 본다.
예를 들어, CPython은 OpenSSL 빌드를 포함해 배포되며, OpenSSL에는 정기적으로 보안 권고가 나온다. 그러나 CPython이 OpenSSL에 노출되는 범위는 대부분 SSL/TLS와 X.509 API의 일부에 한정되어 있으므로, 이런 표면 바깥의 취약점은 CPython에 대한 합리적인 재보고가 되지 않는다.
아마도 가장 논쟁적이고, 또 가장 개인적인 내 브로카르일 것이다. 설명된 동작이 소프트웨어가 어떤 표준이나 명세를 올바르게 준수한 직접적인 결과라면, 그 취약점 보고는 안전하게 기각할 수 있다. 이런 경우 취약점이 있다면 그것은 구현이 아니라 표준 자체 안에 있다.
예시는 다음과 같다.
견고성의 관점에서, 요청 라인을 수신하고 파싱할 것으로 예상하는 서버는 요청 라인보다 앞서 수신된 최소 하나의 빈 줄(CRLF)을 무시해야 한다.
시작 줄과 헤더 필드의 줄 종료자는 CRLF 시퀀스이지만, 수신자는 단일 LF를 줄 종료자로 인식하고 그 앞선 CR을 무시할 수도 있다.
여기서의 미묘한 점은, 표준이 요구하는 것보다 더 엄격하기로 선택한 구현이라면 그 의도된 엄격성이 깨졌을 때 취약하다고 보아야 한다는 것이다.
메인테이너는 그 결과가 취약점 자체의 결과보다 더 나쁜 취약점 보고를 거부해야 한다(또는 다투어야 한다).
이의 고전적 예시는 ReDoS “취약점”으로, 특히 “서비스 거부”의 영향이 미미한 맥락에서 그렇다4. 이런 보고는 보통 메인테이너가 분류하는 데 적지 않은 시간과 노력을 들이게 만들고, 이어서 하위 사용자들 이 이를 완화하는 데에도 적지 않은 시간과 노력을 들이게 만든다. 결과적으로 사실상 공동체 자체에 대한 서비스 거부를 일으킨다.
CVE-2026-4539는 최근의 사례다. 익명의 제보자가 pygments에 대해 VulDB를 통해 CVE를 제출했는데, 메인테이너나 공동체의 검토를 건너뛴 것으로 보인다. 이 보고에는 수정된 버전도 수반되지 않았고(헛소리이며 pygments 자체 보안 정책을 무시하기 때문이다), 그럼에도 수만 개의 하위 의존성에 “중간” 심각도 취약점 경고를 켜 버려 상당한 혼란을 야기했다.
2026년 현재의 현상 유지는, 적대적인 제보자가 메인테이너를 완전히 우회할 때 이런 종류의 스팸에 대해 이의를 제기할 책임을 메인테이너에게 부당하게 떠넘기는 CVE 생태계를 보여 준다.
취약점 보고(그리고 그 보고에 대한 CVE나 기타 식별자)의 존재는, 취약점이 존재하기 위한 필요조건도 충분조건도 아니다.
이것은 양쪽 모두에 해당한다. 많은 취약점들(아마 대다수)은 결코 “공식적으로” 보고되지 않으며, 많은 공식 보고는 실제로 의미 있는 취약점을 설명하지도 않는다(위에서 본 것처럼). 따라서 보고의 존재와 취약점의 존재 사이의 관계에 대해, 검증되지 않은 어떤 가정도 해서는 안 된다.
이 역시 또 다른 불행한 현상 유지다. 이는 취약점 보고 생태계 안의 (의도적인 것으로 보이는) 전략적 모호성에서 비롯된다. MITRE 같은 파트너는 고품질 취약점 정보의 출처로 인식되는 이점을 누리는 동시에, 취약점 주장 에 대한 안정적인 식별자 외에는 아무것도 전달할 책임이 없다고 발뺌할 수도 있기 때문이다.
스팸, “beg bounty” 제출물, 그리고 점점 늘어나는 무성의한 LLM 제출물.↩
브로카르는 그 자체로 법도 아니다.↩
관리형 라이브러리에서 예외를 발생시키는 라이브러리/컴포넌트에 대한 취약점 보고는 매우 흔하며, 종종 해당 언어의 실행 계약에 대한 근본적인 오해를 드러낸다. 예를 들어 Python에서 추정되는 실행 계약은 예외가 어느 지점에서든 발생할 수 있으며, 시스템은 항상 예외를 처리할 준비가 되어 있어야 한다는 것이다. 이것은 모든 코드 블록마다
try..except
가 필요하다는 뜻은 아니지만, 모든 예외는 결국 처리되며, 따라서 취약점은 예외가 잘못 처리될 때만 나타난다는 뜻이다.↩