육아를 마치고 자려고 누워서 잠시 나무위키를 켰는데 실시간 인기 검색어에 PHP가 있었다.
무슨 일인가 하고 눌러 보니 PHP 원격 코드 실행 취약점이었다.


최근에 만든 https://simplelife.ai.kr 이 PHP 기반 워드프레스다. 그래서 새벽에 Claude Code와 같이 바로 조치했다.
요약
- 운영 서버는 Docker 공식
wordpress:php8.3-apache, PHP 8.3.35였다. 취약 버전이고, 구성이 가장 위험하다는 Apache mod_php였다 - PHP 8.3.36은 10/21 23:00(KST)에 나왔지만 Docker Hub 공식 이미지는 아직 8.3.35였다
- 패치 전까지는 Cloudflare 규칙과 PHP 설정으로 공격 경로를 줄였다
- 공식 빌드 파일 그대로 PHP 8.3.36을 직접 빌드해서 워드프레스 이미지를 만들고 운영에 갈아 끼웠다
- 대응 시작부터 운영 반영까지 1시간 31분 걸렸다. 그중 52분은 맥에서 amd64 이미지가 빌드되기를 기다린 시간이고, 손으로 한 작업은 40분 정도다. 예전 같으면 공식 이미지가 나올 때까지 기다렸을 것이다
1. 취약점과 내 서버 상황
| 항목 | 내용 |
|---|---|
| CVE | CVE-2026-109618, CVE-2026-109619 (CVSS v4 9.2) |
| 수정 버전 | 8.5.12, 8.4.27, 8.3.36, 8.2.35 (2026-10-21 14:00 UTC) |
| 109618 | multipart 파싱 중 메모리 고갈(OOM) 뒤 정리 과정의 메모리 오염 |
| 109619 | 요청이 끝난 뒤 남은 전역 상태를 같은 워커의 다음 요청이 사용 |
| 위험한 구성 | Apache mod_php. 로그인 없이, 워드프레스 코드가 돌기 전에 터진다 |
서버에서 확인한 값이다.
$ docker compose exec wordpress php -v
PHP 8.3.35 (cli) (built: Oct 6 2026 01:28:49) (NTS)
$ docker pull wordpress:php8.3-apache && docker run --rm wordpress:php8.3-apache php -v
PHP 8.3.35 (cli) (built: Oct 21 2026 00:33:53) (NTS) # 공식 이미지도 아직 8.3.35
PHP 개발자들은 설정으로는 못 막고 업데이트만이 답이라고 했다.
그래서 패치 전까지는 위험을 줄이고, 패치는 최대한 빨리 하는 두 갈래로 갔다.
판단은 Claude가 정리하고 Gemini·Codex로 교차 검증했다. 세 모델 모두 "패치가 유일한 해결"이라는 데 동의했다. 다만 Gemini는 "php-fpm으로 바꾸면 무력화된다"고 했는데, 공식 권고상 FPM도 영향 범위라서 틀린 답이었다. 교차 검증을 하는 이유다.
2. 패치 전 임시 조치
1) 침해 흔적 점검
조치 전에 이미 뚫렸는지부터 봤다.
- 최근 3일 사이 바뀐
.php4개: 저장소 파일과 SHA256 해시가 같았다 (내가 배포한 테마·mu-plugin) wp-content/uploads안.php: 없음/tmp: 비어 있음- 최근 48시간 로그의 segfault·메모리 초과: 0건
- POST 요청:
wp-cron.php와 REST API뿐
2) Cloudflare 기본 보호 확인
원본 서버는 Cloudflare Tunnel로만 공개되어 있어 모든 웹 요청이 Cloudflare를 거친다.
Cloudflare는 PHP2Shell 관리형 규칙을 자동으로 배포했다.


Cloudflare도 배너에 "위험은 줄이지만 PHP 취약점을 고치지는 않는다"고 적어 두었다.
3) multipart 요청 차단 (CVE-2026-109618)
109618의 주 경로는 multipart/form-data 파싱이다. 공식 권고도 "앱이 안 쓰면 앞단에서 막으라"고 한다.
내 사이트에서 multipart를 쓰는 건 사진 업로드뿐이라, 집 IP가 아닌 곳의 multipart 요청을 막는 사용자 규칙을 넣었다.
(any(lower(http.request.headers["content-type"][*])[*] contains "multipart")
and not ip.src eq <집 IP>)
→ Block
업로드 경로만 막으면 되지 않나 싶지만, PHP는 인증 확인 전에 요청 본문부터 해석한다. 그래서 경로가 아니라 IP로 걸었다.
집에서 multipart POST → 200
다른 IP(Lightsail)에서 multipart → 403
다른 IP에서 일반 GET → 200
4) 세션 업로드 진행률 끄기 (CVE-2026-109619 일부)

워드프레스는 이 기능을 쓰지 않는다. 109619의 한 경로만 막고 109618에는 효과가 없어서, 어디까지나 보조 조치다.
session.upload_progress.enabled => Off => Off
3. PHP 8.3.36 워드프레스 이미지 직접 빌드
공식 wordpress 이미지는 결국 php:8.3-apache 위에 확장 모듈과 워드프레스를 얹은 것이다. Docker 공식 빌드 저장소(docker-library)의 Dockerfile은 이미 8.3.36으로 바뀌어 있었다. Docker Hub에 이미지가 아직 안 올라왔을 뿐이다.
wordpress-patched/
├── php/ # docker-library/php 8.3/trixie/apache 그대로
└── wordpress/ # docker-library/wordpress latest/php8.3/apache, FROM 한 줄만 수정
# wordpress/Dockerfile
FROM simplelife/php:8.3.36-apache # 원래: FROM php:8.3-apache
php Dockerfile은 php.net 원본을 SHA256과 GPG 서명으로 검증한 뒤 컴파일한다. 직접 빌드라도 출처 검증은 공식 이미지와 같다.
1) 로컬(맥, arm64) 빌드와 테스트
docker build -t simplelife/php:8.3.36-apache php # 272초
docker build -t simplelife/wordpress:php8.3.36-apache wordpress # 97초
PHP 8.3.36 (cli) (built: Oct 21 2026 16:31:20) (NTS)
로컬 스택을 이 이미지로 띄워서 확인했다.
- PHP 모듈 목록: 운영과 같음 (gd, imagick, intl, mysqli, zip, opcache 등)
/,/wp-login.php,/sitemap_index.xml,/wp-json/: 200/wp-admin/: 302,/xmlrpc.php: 403 (기존 차단 유지)- 비로그인 미디어 업로드: 401 (multipart 정상 파싱)
- 에러 로그 fatal·segfault: 0
2) 운영(amd64) 빌드와 교체
운영 서버는 1GB·2코어라 거기서 PHP를 컴파일하면 사이트가 느려진다. 그래서 맥에서 buildx --platform linux/amd64로 빌드했다. Rosetta 없는 QEMU 에뮬레이션이라 php 단계만 31분 걸렸다. 네이티브로는 4.5분이던 작업이다.
wordpress 단계에서 한 번 더 막혔다. imagick을 받는 pecl install이 pecl.php.net 504로 실패했다. 보안 릴리스 직후라 PHP 쪽 서버가 몰린 것 같다. 같은 3.8.1을 공식 GitHub 저장소 태그에서 받아 빌드하도록 그 줄만 바꿨다.
# pecl install imagick-3.8.1 대신
mkdir -p /usr/src/imagick; \
curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.1.tar.gz | tar -xz -C /usr/src/imagick --strip-components=1; \
docker-php-ext-install -j "$(nproc)" /usr/src/imagick; \
다시 빌드하는 데 10분 걸렸다. 이미지는 790MB였고, 서버로 보내는 데 31초 걸렸다.
docker save simplelife/wordpress:php8.3.36-apache-amd64 | gzip | ssh simplelife 'gunzip | docker load'
compose는 이미지를 .env에서 고를 수 있게 한 줄 바꿨다. 공식 이미지가 나오면 .env에서 한 줄만 지우면 원래대로 돌아간다.
image: ${WP_IMAGE:-wordpress:php8.3-apache}
$ echo "WP_IMAGE=simplelife/wordpress:php8.3.36-apache-amd64" >> .env
$ docker compose up -d --no-deps wordpress
$ docker compose exec wordpress php -v
PHP 8.3.36 (cli) (built: Oct 21 2026 16:59:28) (NTS)
교체하는 동안 사이트가 끊긴 시간은 컨테이너 재시작 몇 초였다. 교체 뒤 확인한 결과다.
- PHP 모듈 목록: 교체 전과 같음
/,/sitemap_index.xml,/wp-json/,/robots.txt: 200/wp-login.php,/wp-admin/: Cloudflare Access 로그인으로 302 (기존 설정 그대로), 원본에서 직접 열면 200/xmlrpc.php: 403- 비로그인 미디어 업로드: 401
- 에러 로그 fatal·segfault: 0
- 메모리: 909MB 중 사용 502MB (교체 전과 비슷)
4. 걸린 시간
| 시각(KST) | 한 일 |
|---|---|
| 10/21 23:00 | PHP 8.3.36 공개 |
| 10/22 00:57 | 대응 시작: 서버 버전 확인, Gemini·Codex 교차 검증 |
| 01:01 | 침해 흔적 점검 끝 - 이상 없음 |
| 01:05 | session.upload_progress 끄기 반영 |
| ~01:15 | Cloudflare 기본 보호 확인 |
| ~01:25 | multipart 차단 규칙 반영 |
| 01:26~01:34 | 로컬 빌드(6분)·테스트 |
| 01:35~02:15 | 맥에서 amd64 php 빌드 (31분, 에뮬레이션) |
| ~01:50 | 충전기 꽂고 잠. 운영 반영 승인만 해 두고 나머지는 Claude에게 맡김 |
| 02:15~02:27 | pecl 504로 wordpress 단계 실패, GitHub 소스로 바꿔 다시 빌드 (10분) |
| 02:28 | 운영 이미지 교체, PHP 8.3.36 확인 |
| 02:30 | 초안 정리·push 뒤 맥 잠자기 |
PHP 8.3.36이 나오고 3시간 28분, 대응을 시작하고 1시간 31분 뒤에 운영에 반영했다. 빌드를 기다린 시간을 빼면 손으로 한 작업은 40분 정도였다. 공식 wordpress:php8.3-apache는 이 시각까지도 8.3.35였다.
5. 남은 일
- 공식
wordpress:php8.3-apache가 8.3.36 이상이 되면.env의WP_IMAGE줄을 지우고 공식 이미지로 되돌린다 - 그때 Cloudflare 임시 규칙(multipart 차단)도 지운다. 남겨 두면 집 밖에서 사진을 못 올린다
session.upload_progress.enabled = 0은 그대로 둔다
이번에 Node.js로 갈아탈까도 잠깐 고민했다. Gemini·Codex와 같이 따져 본 결론은 "아니다"였다.
런타임을 바꿔도 공개 서버에서 코드가 돌고 패치를 남의 이미지에 기다리는 구조는 그대로라서다.
바꾼다면 서버 없는 정적 사이트 쪽이 맞다. 이건 따로 정리할 생각이다.
참고
'AI' 카테고리의 다른 글
| [AI 자동화] Claude·Gemini 구독으로 트렌드 리포트 파이프라인 만들기 (0) | 2026.10.19 |
|---|