KIMKYUTAE.COM · ©
PHP 긴급 취약점 대응부터 Debian 자동 보안 업데이트까지
2026년 10월 21일, PHP에서 인증 없이 원격 코드 실행으로 이어질 수 있는 치명적인 취약점 두 건이 공개되었다. 워드프레스 자체의 취약점은 아니지만, 워드프레스가 PHP 위에서 동작한다는 점에서 그냥 지나칠 수 없는 문제였다.
내 서버의 PHP는 보안패치가 적용된 버전임을 확인했고, 이참에 PHP 8.4로 전환했다. 이후 운영체제 보안 업데이트와 커널을 점검하고, 마지막에는 자동 보안 업데이트와 필요할 때만 다음 날 새벽 4시에 재부팅하는 설정까지 적용했다.
이 글은 내가 진행한 작업을 정리한 기록이다. 아래 명령어는 Debian 12(Bookworm) + Apache + PHP-FPM 구성임을 참고하자.
PHP 취약점
이번에 공개된 것은 CVE-2026-109618과 CVE-2026-109619이다. 전자는 메모리 부족 상황의 예외 처리에서 메모리 손상을 일으킬 수 있는 문제이고, 후자는 해제된 메모리를 잘못 참조하는 Use-After-Free 계열의 문제다. 둘 다 CVSS 9.2(Critical)로 평가됐고, 조건이 맞으면 인증되지 않은 요청만으로 원격 코드 실행이 가능하다.
| PHP 계열 | 해결 버전 |
|---|---|
| 8.2 | 8.2.35 이상 |
| 8.3 | 8.3.36 이상 |
| 8.4 | 8.4.27 이상 |
| 8.5 | 8.5.12 이상 |
PHP 8.1 이하의 업스트림 보안 지원은 종료됐다. 배포판 공급자가 별도 백포트를 제공하는 경우도 있으므로, 단순히 php -v의 버전 숫자만이 아니라 설치 패키지의 보안 패치 내역도 확인해야 한다. 이번 작업에서는 먼저 PHP 8.2.35 패치 적용을 확인했고, 이후 장기적인 유지보수를 고려해 PHP 8.4.27로 변경했다.
사전 점검과 백업
OS와 PHP 버전, Apache의 PHP 실행 방식부터 확인했다. 운영 중인 서버라면 먼저 인스턴스 스냅샷과 데이터베이스 백업을 진행한다.
cat /etc/os-release
uname -r
php -v
dpkg-query -W 'php8.2-*'
sudo apache2ctl -M 2>/dev/null | grep -E 'mpm_|proxy_fcgi|php.*module'
sudo grep -RInE 'php8\.2|php-fpm\.sock|SetHandler' /etc/apache2/sites-enabled/ /etc/apache2/conf-enabled/
free -h
Debian 12에서 PHP 8.4 설치
Debian 12의 기본 저장소는 PHP 8.2 계열이어서 PHP 8.4는 별도 저장소가 필요하다.
저장소를 등록한 뒤 처음 설치를 시뮬레이션했을 때는 필요하지 않은 PHP 8.5 패키지와 Apache용 mod_php까지 함께 설치 대상으로 나타났다. 이후 APT 우선순위를 조정해 PHP 8.4와 필수 공통 패키지만 선택되도록 했다.
sudo apt update
sudo apt install -y ca-certificates curl lsb-release
curl -fsSLo /tmp/debsuryorg-archive-keyring.deb \
https://packages.sury.org/debsuryorg-archive-keyring.deb
sudo dpkg -i /tmp/debsuryorg-archive-keyring.deb
echo 'deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ bookworm main' \
| sudo tee /etc/apt/sources.list.d/sury-php.list
sudo apt update
단순히 SURY 패키지 전체에 우선순위를 높게 주면 기존 Debian 패키지까지 교체될 수 있다. 아래처럼 PHP 8.4와 필수 의존성인 php-common만 우선하도록 제한했다. php-common은 PHP 8.4 설치에 필요한 버전 조건 때문에 업그레이드가 필요했다.
sudo tee /etc/apt/preferences.d/90-sury-php84.pref >/dev/null <<'EOF'
Package: php8.4* php-common
Pin: origin "packages.sury.org"
Pin-Priority: 700
Package: *
Pin: origin "packages.sury.org"
Pin-Priority: 100
EOF
apt-cache policy php8.4-fpm php-common
설치할 PHP 확장은 실제로 사용하던 확장을 기준으로 정했다.
sudo apt-get -s --no-install-recommends install \
php8.4-cli php8.4-fpm php8.4-bz2 php8.4-curl \
php8.4-gd php8.4-mbstring php8.4-mcrypt \
php8.4-mysql php8.4-xml php8.4-zip php8.4-opcache
# Run only after reviewing the simulation output:
sudo apt-get --no-install-recommends install \
php8.4-cli php8.4-fpm php8.4-bz2 php8.4-curl \
php8.4-gd php8.4-mbstring php8.4-mcrypt \
php8.4-mysql php8.4-xml php8.4-zip php8.4-opcache
실제 시뮬레이션에서는 버전 미지정 메타패키지 php-mcrypt가 제거 대상으로 표시됐다. 이슈 발생 시 기존 PHP로 롤백할 계획이 있으므로 apt autoremove는 바로 실행하지 않았다.
php8.4 -v
sudo /usr/sbin/php-fpm8.4 -t
systemctl is-active php8.4-fpm
ls -l /run/php/php8.4-fpm.sock
PHP-FPM 설정과 Apache 전환
Apache에서는 가상호스트 설정에 PHP 버전별 소켓 경로가 직접 지정되어 있는지를 먼저 확인했다.
sudo systemctl restart php8.4-fpm &&
sudo a2disconf php8.2-fpm &&
sudo a2enconf php8.4-fpm &&
sudo apache2ctl configtest &&
sudo systemctl reload apache2
systemctl is-active apache2 php8.4-fpm
# Replace the example domain with your site address:
curl -I https://example.com
관리자 화면의 ‘도구 → 사이트 건강 → 정보 → 서버’에서 PHP SAPI와 버전을 확인하고, 게시물 편집·이미지 업로드·캐시 재생성도 테스트했다. 이때 실제 웹 요청이 PHP 8.4의 fpm-fcgi로 처리되는 것을 확인했다.
문제가 생겼을 때 기존 PHP 8.2-FPM과 패키지가 남아 있다는 전제로 Apache 연결만 기존 PHP 8.2로 돌리는 명령은 아래와 같다.
sudo systemctl start php8.2-fpm
sudo a2disconf php8.4-fpm
sudo a2enconf php8.2-fpm
sudo apache2ctl configtest &&
sudo systemctl reload apache2
Debian 보안 업데이트
PHP 작업을 마치고 운영체제도 확인했다. Debian 12는 2026년 7월부터 LTS 보안 지원 단계에 있으며 지원 예정일은 2028년 6월 30일이다. 이번 점검에서는 커널 보안 패키지가 설치돼 있었지만 실제 실행 중인 커널이 이전 버전이어서 재부팅이 필요했다. 또 XML 파싱 라이브러리의 보안 업데이트가 한 건 남아 있어 적용했다.
uname -r
apt-cache policy linux-image-cloud-amd64
apt list --upgradable
# Simulate a targeted security library update:
sudo apt-get -s install --only-upgrade libexpat1
# Run after reviewing package changes:
sudo apt install --only-upgrade libexpat1
# Check reboot requirement and kernel packages:
test -f /run/reboot-required && echo "Reboot required" || echo "No reboot flag"
dpkg -l 'linux-image-*cloud-amd64'
업데이트된 커널이 설치되어 있어도 재부팅 전까지는 기존 커널이 실행될 수 있다.
sudo apt install debian-security-support
check-support-status
# After reboot:
uname -r
systemctl is-active apache2 php8.4-fpm
apt list --upgradable
자동 보안 업데이트와 예약 재부팅
매번 취약점 공지를 보고 수동으로 확인하는 방식에는 한계가 있다. 그래서 Debian 공식 보안 저장소에서 제공하는 패치는 unattended-upgrades로 매일 자동 설치하도록 바꿨다. 반면 SURY의 PHP 8.4 업데이트는 별도 검증이 필요하다고 판단해 자동 적용 대상에서 제외했다.
자동 재부팅은 ‘매일 4시’가 아니라 업데이트 이후 재부팅 필요 표시가 있고, 그 표시가 한국시간 기준 이전 날짜에 생성된 경우에만 오전 4시(KST)에 재부팅하도록 구성했다. 서버의 시간대가 UTC여도 타이머는 Asia/Seoul을 기준으로 실행된다. 이 방식은 패치 설치 날짜와 재부팅 날짜를 분리하려는 운영 선택이며, Linux나 Debian의 일반적인 기본 설정은 아니다.
아래는 Debian 12와 systemd를 사용하는 서버에서 검증한 예시다.
sudo bash <<'SETUP'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
echo "===== 1. Install unattended-upgrades ====="
apt-get update
apt-get install -y unattended-upgrades
echo "===== 2. Backup existing configuration ====="
for f in \
/etc/apt/apt.conf.d/20auto-upgrades \
/etc/apt/apt.conf.d/52-unattended-security
do
if [ -f "$f" ] && [ ! -f "$f.pre-auto-reboot.bak" ]; then
cp -a "$f" "$f.pre-auto-reboot.bak"
fi
done
echo "===== 3. Enable automatic security updates ====="
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
echo "===== 4. Configure security update policy ====="
cat > /etc/apt/apt.conf.d/52-unattended-security <<'EOF'
#clear Unattended-Upgrade::Origins-Pattern;
#clear Unattended-Upgrade::Allowed-Origins;
Unattended-Upgrade::Origins-Pattern {
"origin=Debian,codename=${distro_codename},label=Debian-Security";
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Dependencies "false";
Unattended-Upgrade::Remove-New-Unused-Dependencies "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "false";
EOF
echo "===== 5. Create conditional reboot script ====="
cat > /usr/local/sbin/security-reboot-nextday <<'EOF'
#!/bin/bash
set -euo pipefail
export TZ=Asia/Seoul
# Exit if reboot is not required
if [ ! -f /run/reboot-required ]; then
logger -t security-reboot "No reboot required"
exit 0
fi
# Determine today's midnight in KST
today=$(date +%F)
midnight=$(date -d "$today 00:00:00" +%s)
required_at=$(stat -c %Y /run/reboot-required)
# Do not reboot on the day the flag was updated
if [ "$required_at" -ge "$midnight" ]; then
logger -t security-reboot \
"Reboot required today; postponed until next day 04:00 KST"
exit 0
fi
logger -t security-reboot \
"Reboot required; rebooting at 04:00 KST"
systemctl --no-block reboot
EOF
chmod 755 /usr/local/sbin/security-reboot-nextday
echo "===== 6. Configure kernel update reboot flag ====="
install -d -m 755 /etc/kernel/postinst.d
cat > /etc/kernel/postinst.d/zz-security-reboot-flag <<'EOF'
#!/bin/sh
set -e
# Mark reboot required after a new kernel installation
[ "${1:-}" = "$(uname -r)" ] && exit 0
touch /run/reboot-required
EOF
chmod 755 /etc/kernel/postinst.d/zz-security-reboot-flag
echo "===== 7. Create systemd reboot service ====="
cat > /etc/systemd/system/security-reboot-nextday.service <<'EOF'
[Unit]
Description=Conditional security reboot at 04:00 KST
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/security-reboot-nextday
EOF
echo "===== 8. Create daily reboot timer ====="
cat > /etc/systemd/system/security-reboot-nextday.timer <<'EOF'
[Unit]
Description=Daily security reboot check at 04:00 KST
[Timer]
OnCalendar=*-*-* 04:00:00 Asia/Seoul
AccuracySec=1s
RandomizedDelaySec=0
Persistent=false
Unit=security-reboot-nextday.service
[Install]
WantedBy=timers.target
EOF
echo "===== 9. Enable timers ====="
systemd-analyze verify \
/etc/systemd/system/security-reboot-nextday.service \
/etc/systemd/system/security-reboot-nextday.timer
systemctl daemon-reload
systemctl enable --now \
apt-daily.timer \
apt-daily-upgrade.timer \
security-reboot-nextday.timer
echo "===== 10. Verify ====="
systemctl list-timers --all --no-pager \
apt-daily.timer \
apt-daily-upgrade.timer \
security-reboot-nextday.timer
systemd-analyze calendar '*-*-* 04:00:00 Asia/Seoul'
echo "===== Configuration completed ====="
SETUP
이 스크립트는 /run/reboot-required의 수정 시각을 기준으로 판단한다. 따라서 자동 업데이트뿐 아니라 수동 작업에서 생성한 재부팅 요청에도 반응할 수 있다. 반대로 어떤 패키지가 이 파일을 생성하지 않으면 자동 재부팅 조건에도 잡히지 않는다.
적용 뒤에는 허용된 저장소와 타이머를 확인하고, --dry-run으로 실제 업데이트 없이 동작을 살폈다. 당시 출력에서는 Debian 보안 저장소만 자동 설치 대상으로 선택됐고 SURY 저장소는 제외됐다. 재부팅 타이머도 한국시간 새벽 4시를 가리켰다.
echo "===== Update policy ====="
apt-config dump | grep -E \
'APT::Periodic|Unattended-Upgrade::(Origins-Pattern|Allowed-Origins|Automatic-Reboot)'
echo "===== Timers ====="
systemctl list-timers --all --no-pager \
apt-daily.timer apt-daily-upgrade.timer security-reboot-nextday.timer
echo "===== Security update dry-run ====="
sudo unattended-upgrade --dry-run --debug 2>&1 | tail -n 30
echo "===== Logs ====="
sudo tail -n 30 /var/log/unattended-upgrades/unattended-upgrades.log
sudo journalctl -t security-reboot -n 30 --no-pager
예약 재부팅을 중지해야 한다면 아래 명령으로 타이머만 끄면 된다.
sudo systemctl disable --now security-reboot-nextday.timer
# Re-enable when needed:
sudo systemctl enable --now security-reboot-nextday.timer






