다음에 올 것을 만들어가는 기술에 대한 심층 기사.

PHP CVE 패치 창: 임시방편보다 서비스 차단이 나은 이유

사전 공지된 치명적 PHP CVE 패치 창에서 악용 가능성을 평가하고 빠르게 패치하는 방법을 정리했습니다.

폭풍이 다가올 때 스스로 불을 끄는 도시 건물들, 선제적 서비스 차단을 상징
때로는 가장 안전한 설정이 '끔'입니다. 노출된 채 기다리느니 기관 전체가 불을 껐습니다.

PHP 프로젝트가 치명적인 보안 릴리스가 곧 나온다고 발표했을 때, 실제 패치가 존재하기도 전이었는데, 인터넷은 늘 하던 일을 했습니다. 생산적인 패닉이죠. 네덜란드 정부 로그인 포털, 국영 방송사, 의회 웹사이트가 선제적으로 먼저 내려갔습니다. Cloudflare는 WAF 규칙을 급히 손봤고, 시스템 관리자들은 Reddit에서 반쯤만 검증된 php.ini 완화책을 주고받았으며, 소셜 미디어에서는 익스플로잇 지침이 돌았습니다. 이 모든 과정을 지켜보며 얻은 제 역발상 결론은 이렇습니다. 그냥 전부 꺼버린 팀이 그 자리에서 가장 똑똑한 결정을 내린 겁니다. 부분적인 기교로 대응한 팀들은 대부분 보안 쇼(security theater)를 하고 있었죠. 이 주장은 근거를 댈 필요가 있고 분명 한계도 있습니다. 그럼 치명적인 PHP CVE 패치 창이 패치 전략에 대해 실제로 무엇을 가르쳐 주는지 살펴보겠습니다.

사전 공지된 패치가 양날의 검인 이유

취약점 공개의 일반적인 흐름은 패치가 먼저, 상세 내용은 나중입니다. 이번 사태는 그 순서를 뒤집었습니다. 유출된 영상 때문에 PHP 팀이 손을 쓸 수밖에 없었고, 패치보다 먼저 발표가 나갔습니다. 실제 패치 버전(8.5.12, 8.4.27, 8.3.36, 8.2.35)은 며칠 뒤 UTC 기준으로 정해진 시각에 나오도록 예약됐죠. “심각한 문제가 존재한다고 확인했다”와 “여기 수정본이 있다” 사이의 이 간격이 현대 취약점 관리에서 가장 위험한 시기입니다. 방어자와 공격자가 같은 시계를 보고 있기 때문입니다.

많은 실무자들은 패치를 즉시 배포했어야 한다고 주장했습니다. 하지만 메커니즘을 따져 봅시다. 공개된 패치는 사실상 리버스 엔지니어링 키트입니다. 패치된 소스를 이전 릴리스와 비교(diff)하면 취약한 코드 경로가 몇 줄로 좁혀지고, LLM 기반 익스플로잇 생성 덕분에 “diff 공개”에서 “동작하는 익스플로잇”까지 걸리는 시간이 며칠에서 몇 시간으로 줄었습니다. PHP 릴리스 매니저들도 공지가 버그 유형을 확인해 준 뒤 패치 전에 익스플로잇이 얼마나 빨리 나왔는지 놀랐다고 인정했습니다. 일찍 릴리스한다고 세상이 조율되지는 않습니다. 그저 수많은 패치되지 않은 인스턴스를 두고 공격 타이머만 켜질 뿐이고, 그 서버를 운영하는 사람들은 자고 있거나, 회의 중이거나, 변경 관리 대기열에 갇혀 있습니다.

발표된 고정 릴리스 시각은 어색하게 느껴졌지만 사실상 조율 장치였습니다. 모두가 14:00 UTC에 패치합니다. 심각하게 노출된 곳은 모두 그 전에 서비스를 내립니다. 이렇게 동기화된 창이 특정 순간에 취약하면서 온라인 상태인 서버의 총 개수를 최소화합니다. 이는 보안 전반에서 쓰이는 조정된 공개(coordinated disclosure)와 같은 논리입니다. 지저분하고 불편하지만, 보통은 그나마 최선의 선택지죠.

정보가 거의 없을 때 악용 가능성 평가하기

이 창에서 가장 어려운 부분은 불완전한 데이터로 하는 트리아지였습니다. 권고문은 일부러 모호했는데도 팀은 결정해야 했습니다. 끌 것인가, 완화할 것인가, 기다릴 것인가. 제가 실제로 쓸 프레임워크는 다음과 같고, 버그의 모양보다는 노출의 모양에 관한 것입니다.

  • 공격 표면부터 보세요. 초기 신호는 파일 업로드 처리와 세션 업로드 진행 상태를 가리켰습니다. 익명 사용자가 multipart POST 요청을 보낼 수 있다면(문의 폼, 아바타 업로드, 가져오기 도구 등) 대기열 맨 앞에 서 있는 겁니다. VPN과 SSO 뒤에 있는 내부 관리 도구는 완전히 다른 위험 등급입니다.
  • 인터프리터 도달 범위. PHP 코어의 버그는 10년 동안 모든 버전에 영향을 줍니다. 즉 노출은 대략 “요청으로 도달 가능한 모든 PHP 프로세스”입니다. 여기서는 분석보다 인벤토리가 더 중요합니다. 대부분의 조직은 잊고 있던 곳에서 PHP가 돌고 있다는 걸 발견했습니다. 오래된 마이크로사이트, 모니터링 대시보드, 2019년에 만든 마케팅용 워드프레스 같은 것들이요.
  • 피해 반경(blast radius). 과거 아키텍처의 죄가 여기서 돌아옵니다. 토론 중 한 실무자가 말했듯, 전통적인 PHP 앱은 사용자와 관리자가 공유하면서 모든 것을 읽고 쓸 수 있는 DB 역할로 실행됩니다. 폭주한 PHP 프로세스는 앱 하나만 뚫는 게 아니라 DB 전체를 빼내 갈 수 있습니다. 앱이 테넌트별 역할과 행 수준 보안(row-level security)을 쓰고 있다면 최악의 시나리오는 크게 줄어듭니다.
  • 탐지 체계. “이 호스트에 웹쉘이 생기면 우리가 알아챌 수 있을까?”에 답할 수 없다면 패치 속도가 유일한 방어입니다. 이는 더 넓은 사각지대와 연결됩니다. 대부분의 팀은 인증 이벤트는 기록하지만 세션이 수립된 이후에 무슨 일이 일어나는지는 기록하지 않습니다.

이 네 가지 관점을 적용하면 결정은 대개 저절로 나옵니다. 노출도가 높고, 피해 반경이 넓고, 탐지가 안 되나요? 끄세요. 공격 표면이 좁고, 권한이 격리돼 있고, 로깅이 잘 되어 있나요? WAF 규칙과 눈 크게 뜨고 지켜보는 것으로 패치 창을 버틸 수 있을 겁니다.

완화책의 함정: 부분적인 수정이 대부분 기대에 못 미치는 이유

발표 후 몇 시간 만에 포럼에서 한 가지 완화책이 퍼졌습니다. php.ini에서 세션 업로드 진행 상태와 파일 업로드를 끄라는 것이었죠. 사람들은 충실히 설정하고 phpinfo()로 확인한 뒤 안심했습니다. 그러자 한 PHP 릴리스 매니저가 INI 설정만으로는 보호되지 않는다, PHP를 패치해야 한다고 단호하게 말했습니다. 이 한 번의 대화가 완화책의 함정을 보여줍니다. 설정 변경은 행동하는 느낌을 주지만, 인터프리터의 메모리 손상 계열 버그 앞에서는 익스플로잇 경로에 대한 추측일 뿐 취약점에 대한 해결책이 아닙니다.

실질적인 효과가 있었던 유일한 완화책은 엣지에서 요청 형태를 필터링하는 것이었습니다. 익스플로잇이 multipart 요청 본문을 필요로 하고 사이트가 파일 업로드를 필요로 하지 않는다면, 해당 콘텐츠 타입을 아예 차단하면 공격 경로를 막을 수 있습니다. 방해하기를 바라는 게 아니라 제거하는 겁니다. Apache에서는 다음과 같은 rewrite 규칙이 됩니다:

# Block all multipart/form-data requests at the vhost level
RewriteEngine On
RewriteCond %{HTTP_CONTENT_TYPE} ^multipart/form-data [NC]
RewriteRule .* - [R=403,L]
# For nginx, the equivalent lives in the server block:
# if ($http_content_type ~* "^multipart/form-data") { return 403; }

핵심은 조건부 로직이라는 점을 기억하세요. 이 방법은 업로드가 핵심 기능이 아닐 때만 통합니다. 업로드 중심 제품을 운영 중이라면 스스로 DDoS를 만드는 셈입니다. 원격 코드 실행(RCE)보다는 낫다고 할 수 있지만, 이건 보안이 아니라 비즈니스 결정입니다. 그리고 이것도 익스플로잇 형태에 거는 도박입니다. 두 번째 공격 벡터가 나타나면(Cloudflare도 새로운 벡터가 보일 때마다 완화책을 업데이트했다고 보고했습니다) 규칙은 순식간에 쓸모없어집니다. 엣지 필터링은 시간을 벌어 줄 뿐, 안전을 사 주지는 못합니다.

다층 방어를 갖춘 성문이 들어오는 트래픽을 걸러내는 모습, WAF 규칙과 요청 필터링을 상징
엣지 필터링은 치료제가 아니라 병목 지점입니다. 진짜 패치를 기다리는 동안에만 유용합니다.

서비스 중단이라는 불편한 선택

이제 제가 처음에 한 주장입니다. 토론에 참여한 한 엔지니어는 수정이 나올 때까지 고객의 모든 앱을 오프라인으로 돌리는 결정을 내렸고, 그날 하루 종일 전화로 설명하며 보냈습니다. 직업적으로 고통스러운 일이죠. 하지만 옳은 결정이었습니다. 비용의 비대칭이 엄청나기 때문입니다. 몇 시간의 다운타임은 힘든 하루와 어색한 청구서 대화로 끝납니다. 반면 인터프리터 수준 버그에 대한 패치 전 RCE, 공개된 익스플로잇 지침, 대규모 공격을 뿌릴 수 있는 LLM 에이전트가 있는 상황이라면 이는 침해 공개, 포렌식 작업, 그리고 “호스트가 깨끗하다는 걸 증명할 수 없으니 모든 서버를 처음부터 다시 만들어야 하나?”라는 현실적인 질문으로 이어집니다.

마지막 지점은 강조할 가치가 있습니다. 여러 댓글은 합리적인 업그레이드 지시가 “호스트를 불태우고 다시 시작하라”라고 주장했는데, 과장이 아니었습니다. 공유 없는(shared-nothing) PHP-FPM 호스트에서도 사후 익스플로잇 후에는 지속성 수단이 남습니다. cron, 변조된 파일, 오염된 세션, 탈취된 DB 자격 증명 같은 것들이죠. 익스플로잇 공개와 패치 사이 창 동안 온라인 상태였고 패치되지 않았다면, “패치했다”는 “털렸나?”라는 질문에 답하지 못합니다. 알려진 정상 이미지에서 다시 빌드하는 것만이 답합니다. 불변 인프라를 쓰던 팀은 조용히 훌륭한 한 주를 보냈습니다. 패치된 베이스 이미지로 재배포하고, 자격 증명을 교체하고, 다음으로 넘어가면 끝이었죠.

그럼 서비스 중단 논리는 어디서 깨질까요? 세 군데입니다. 첫째, 병원 시스템, 결제 흐름, 긴급 인프라처럼 정말로 중요한 서비스는 다운타임 자체가 안전 비용을 치르므로 전원 스위치가 아니라 계층적 격리가 필요합니다. 둘째, 서비스 중단에 걸리는 시간이 패치 창보다 긴 조직입니다. 릴리스 과정이 3일짜리 수작업이라면 어차피 14:00 UTC를 맞출 수 없었을 겁니다. 셋째, 그리고 가장 뼈아픈 점은 실행 중인 모든 것을 알고 있어야만 끌 수 있다는 것입니다. 서브도메인에 있는 잊힌 워드프레스 인스턴스는 아무도 존재를 기억하지 않기 때문에 저절로 꺼지지 않습니다. 자산 인벤토리는 멋없지만 실제 경계선입니다.

다음 사태 전에 런타임 업데이트 전략 세우기

다음 사태는 반드시 옵니다. 유용한 질문은 이번 창에서 얼마나 잘 대응했는가가 아니라, 다음 창이 지루하게 지나갈 수 있는가입니다. 레버리지가 큰 순서대로 몇 가지 구체적인 조치가 있습니다:

  1. 지원되는 PHP 버전을 추적하고 패치 업데이트를 자동화하세요. 몇 분 만에 패치한 팀은 태그가 찍히면 베이스 이미지가 다시 빌드되고 재배포되는 구조였습니다. PHP 업데이트가 펫(pet) 서버에서 직접 컴파일하는 일이라면 이미 경쟁에서 진 겁니다. 배포판 패키지(Debian, Alpine)는 릴리스 후 몇 시간 안에 수정 버전을 내보냈습니다. 직접 만든 빌드보다 배포판의 보안 주기에 올라타는 편이 낫습니다.
  2. 레거시 서비스 앞에는 관리형 규칙을 갖춘 WAF를 두세요. Cloudflare는 패치가 나오기도 전에 무료와 유료 요금제 모두에서 관리형 WAF 규칙으로 이를 완화했습니다. WAF가 일반적으로 심층 방어인지 보안 쇼인지는 논쟁할 수 있지만, 패치 전 창에서는 분명하게 심층 방어입니다.
  3. 지금 DB의 피해 반경을 줄이세요. 읽기와 쓰기용 역할을 분리하고, 애플리케이션별 자격 증명을 쓰고, 적합한 곳에는 행 수준 보안을 적용하세요. 다음 인터프리터 버그가 자동으로 DB 전체 유출을 뜻하면 안 됩니다. 이것이야말로 진정한 의미의 방어적 엔지니어링입니다. 한 계층이 실패해도 우아하게 기능이 저하되는 시스템 말이죠.
  4. 킬 스위치를 연습하세요. 15분 안에 서비스를 정적 점검 페이지로 전환하는 방법을 문서화하고 테스트해 두세요. 그 런북을 쓰는 시점이 공개 창 한가운데여서는 안 됩니다.
  5. 호스트를 일회용으로 만드세요. 불변 이미지, SSH로 손대는 스노플레이크 서버 금지, 정기적인 자격 증명 교체. 그러면 “불태우고 재빌드”는 위기가 아니라 그냥 배포가 됩니다.

사전 공지된 패치 창은 여러분의 완화책이 얼마나 영리한지를 시험하지 않습니다. 여러분의 평범한 인프라가 얼마나 빠르게 움직일 수 있는지를 시험합니다.

이 창이 실제로 측정한 것

드라마를 걷어내면 이번 사태는 PHP에 대한 테스트가 아니라 운영 성숙도에 대한 스트레스 테스트였습니다. 인터프리터에는 버그가 있었고, 인터프리터는 원래 버그가 있습니다. 생태계 전반에서 극명하게 달랐던 것은 피해 반경과 대응 속도였습니다. 어떤 팀은 과감하게 서비스를 내리고, 조율된 시점에 패치하고, 점심 전에 깨끗한 이미지로 재구축했습니다. 다른 팀은 창 전체를 INI 설정을 만지작거리며 기도하면서 보냈죠. 차이는 보안 감각의 정교함이 아니었습니다. 배포 파이프라인, 자산 인벤토리, 장기적인 침해를 피하기 위해 단기적인 다운타임을 감수할 의지의 차이였습니다.

그래서 저는 처음 주장을 단서와 함께 유지하겠습니다. 치명적이고 사전 공지된 인터프리터 버그가 악용 가능해지기까지 몇 시간 남았고 여러분의 완화책이 추측에 불과하다면, 플러그를 뽑는 것이 어른스러운 선택입니다. 만족스럽지 않고, 데모에서 멋있어 보이지도 않습니다. 하지만 보안 태세는 위기 중에 얼마나 바빠 보이는지로 측정되지 않습니다. 사후에 아무것도 들어오지 않았다는 것을 증명할 수 있는지로 측정됩니다. 이번 주에 실행 중인 모든 PHP 프로세스에 대해 이 질문에 답할 수 없다면, 그것이 진짜로 고쳐야 할 버그입니다.

사고가 해결된 뒤 초록색 상태 모니터가 켜진 조용한 관제실
최고의 사고 대응은 초록불이 켜지고 아무 놀라움 없이 끝나는 대응입니다.