PHP2Shell(CVE-2026-109618·109619) 대응: 서버별 PHP 버전 확인, 패치 반영 검증, php.ini 임시 완화 설정

운영 서버·컨테이너·배포판 패키지에서 실제 버전과 CVE 반영을 확인하고, 업데이트 전까지 앱이 덮어쓸 수 없는 위치에 완화값을 고정하는 절차

PHP2Shell 패치 확인과 임시 완화 썸네일

핵심 요약

  1. 해결 버전은 8.5.12, 8.4.27, 8.3.36, 8.2.35이며, 8.1 이하는 PHP 프로젝트의 업데이트가 없으므로 8.3 이상으로 옮긴다.
  2. 배포판 패키지는 버전 번호 대신 패키지 변경 이력(apt changelog, rpm -q --changelog)에서 CVE 번호로 반영 여부를 확인한다.
  3. 임시 완화값은 php_admin_value·php_admin_flag나 php.ini에 둔다. ini_set()이나 .user.ini에 두면 적용 시점이 늦어 효과가 없다.
  4. php -v는 CLI 기준이므로, 웹 요청을 처리하는 FPM·mod_php 워커에서 버전과 설정값을 따로 확인한다.
  5. 업데이트든 완화 설정이든 모든 워커를 재시작해야 반영되며, 완화 설정만으로는 안전해지지 않는다.

PHP를 쓰는 서비스를 운영한다면, 2026년 10월 21일 공개된 원격 코드 실행 취약점(이른바 PHP2Shell)에 맞춰 할 일은 세 가지다. 웹 요청을 처리하는 PHP가 실제로 어떤 버전인지 확인하고, 해결 버전으로 올라갔는지 검증하고, 바로 올릴 수 없는 서버에는 임시 완화 설정을 넣는다. 이 글은 이 세 단계를 명령과 설정 파일 단위로 정리한다. 공격 재현 방법은 다루지 않는다.

이번 취약점에서 확인할 사실

php.net 보안 권고에 따르면 취약점은 두 개다. CVE-2026-109618은 메모리 부족 처리 중 요청 데이터가 덜 초기화된 상태로 남는 문제이고, CVE-2026-109619는 해제된 요청 상태가 같은 워커의 다음 요청에서 다시 쓰이는 문제다. PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed 등 웹 요청을 처리하는 모든 설치가 대상이다. 권고는 mod_php의 위험을 가장 높게 본다. KISA 보호나라 공지는 패치 전에 정보가 새어 나가 공격 시도가 보고됐다고 알렸다. 두 CVE 모두 GHSA-6wmf-4rv5-p3rp 기준 CVSS v4 9.2 등급이다.

브랜치해결 버전보안 지원 종료조치
8.58.5.122029-12-31업데이트
8.48.4.272028-12-31업데이트
8.38.3.362027-12-31업데이트
8.28.2.352026-12-31업데이트 후 8.3 이상 이전 계획
8.1 이하프로젝트 업데이트 없음종료8.3 이상으로 이전

보안 지원 종료일은 PHP Supported Versions 기준이다. 8.1 이하는 PHP 프로젝트가 패키지를 내지 않는다. 7.4·8.0·8.1용 소스 백포트 브랜치(PHP-<버전>-CVE-2026-109618-109619)만 공개돼 있어 직접 빌드해야 한다. 이 경우 브랜치 이전이 현실적인 선택지다.

1단계: 웹 요청을 처리하는 PHP 찾기

서버에 PHP가 여러 개 설치된 경우가 많다. 터미널의 php -v는 CLI 바이너리의 버전일 뿐, 웹 요청을 처리하는 FPM이나 Apache 모듈과 다를 수 있다. 다음 순서로 실제 처리 경로를 찾는다.

  1. 실행 중인 프로세스에서 FPM 마스터와 Apache를 찾는다.
  2. FPM이면 해당 바이너리의 버전을, Apache면 PHP 모듈이 로드돼 있는지 확인한다.
  3. 컨테이너로 배포한다면 운영에 쓰는 이미지 태그 그대로 버전을 확인한다.
# 1) 실행 중인 PHP 처리 프로세스
ps -eo pid,user,cmd | grep -E 'php-fpm|php-cgi|httpd|apache2|frankenphp|lsphp' | grep -v grep

# 2) FPM 바이너리 버전 (이름은 배포판마다 php-fpm, php-fpm8.3 등)
php-fpm8.3 -v
# Apache에 mod_php가 로드돼 있는지
apachectl -M 2>/dev/null | grep -i php

# 3) 운영 이미지의 PHP 버전
docker run --rm --entrypoint php registry.example.com/web:prod -v

외주로 만든 사이트, 이벤트용 임시 서버, 관리자 페이지 전용 서버처럼 목록에서 빠지기 쉬운 시스템도 같은 방식으로 확인한다. 사내 도구를 급하게 띄운 경우라면 사내 도구 보안 설정 점검의 노출 범위 확인 항목을 함께 본다.

2단계: 배포판 패키지는 변경 이력으로 패치 확인하기

Debian·Ubuntu·RHEL 계열 패키지는 보안 수정을 기존 버전 번호에 백포트한다. 그래서 8.3.36보다 낮은 숫자가 보여도 패치가 들어갔을 수 있고, 숫자만으로는 판단할 수 없다. 패키지 변경 이력에서 CVE 번호를 찾는다.

# Debian / Ubuntu
apt changelog php8.3-fpm | grep -E 'CVE-2026-10961[89]'

# RHEL / Rocky / Alma
rpm -q --changelog php-fpm | grep -E 'CVE-2026-10961[89]'

# Alpine (NCSC-NL 목록 기준 php84는 8.4.27-r0에서 수정)
apk info -v | grep '^php'

두 CVE가 모두 나오지 않으면 미패치로 보고 업데이트한다. 업데이트한 뒤에는 FPM과 Apache를 재시작해 기존 워커를 모두 교체한다. 패키지만 바꾸고 재시작하지 않으면 메모리에 올라간 이전 코드가 계속 요청을 처리한다.

sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2   # mod_php를 쓰는 경우

컨테이너는 베이스 이미지 태그를 패치 버전으로 올려 다시 빌드하고 배포한다. php:8.3-fpm처럼 마이너 버전까지만 적은 태그는 빌드 시점마다 내용이 달라진다. 운영 이미지에는 패치 번호까지 적힌 태그나 다이제스트를 쓰고, 이미지 구성 전반은 Docker 배포 환경 구성 글을 참고한다.

3단계: CI와 이미지 빌드에서 버전 하한 검사하기

이번 대응이 끝난 뒤 이전 이미지가 다시 배포되는 일을 막으려면, 빌드 단계에서 버전을 검사하는 스크립트를 둔다. 공식 바이너리나 공식 Docker 이미지처럼 버전 번호가 그대로 올라가는 경우에 맞는 검사다. 배포판 패키지는 2단계 방식으로 확인한다.

<?php
// scripts/check-php-version.php — CI와 Dockerfile의 RUN 단계에서 실행
$fixed = ['8.2' => '8.2.35', '8.3' => '8.3.36', '8.4' => '8.4.27', '8.5' => '8.5.12'];
$branch = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;

if (!isset($fixed[$branch])) {
    fwrite(STDERR, "PHP {$branch}: 지원 종료이거나 목록에 없는 브랜치\n");
    exit(1);
}
if (version_compare(PHP_VERSION, $fixed[$branch], '<')) {
    fwrite(STDERR, 'PHP ' . PHP_VERSION . " 는 {$fixed[$branch]} 미만\n");
    exit(1);
}
echo 'PHP ' . PHP_VERSION . " OK\n";
COPY scripts/check-php-version.php /tmp/
RUN php /tmp/check-php-version.php

8.6 RC처럼 목록에 없는 브랜치는 일부러 실패하게 했다. php.net 권고에 따르면 8.6.0 RC4에 수정이 들어 있으므로, 쓰고 있다면 목록에 추가한다.

업데이트 전까지 넣을 임시 완화 설정

바로 업데이트할 수 없는 서버에는 권고의 완화 설정을 넣는다. php.net과 GHSA 모두 이 설정으로는 일부 공격 경로만 줄어들 뿐 안전해지지 않는다고 명시한다. 업데이트 일정을 먼저 잡고, 그 사이에만 쓰는 조치로 다룬다.

설정을 둘 위치가 중요하다. 권고는 ini_set()이나 FPM의 .user.ini에 두지 말라고 한다. 요청을 해석하기 전에 적용되는 위치여야 효과가 있기 때문이다. FPM 설정 문서에 따르면 php_admin_value·php_admin_flag로 정한 값은 애플리케이션이 ini_set()으로 바꿀 수 없다.

; FPM pool 설정 (예: /etc/php/8.3/fpm/pool.d/www.conf, 경로는 배포판마다 다름)
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 32M          ; 파일 업로드가 없으면 8M
php_admin_value[upload_max_filesize] = 16M    ; post_max_size보다 작게
php_admin_flag[session.upload_progress.enabled] = off
; PHP 8.5만 해당
; php_admin_flag[fatal_error_backtraces] = off
# Apache mod_php (VirtualHost 또는 서버 설정)
php_admin_value memory_limit -1
php_admin_value post_max_size 32M
php_admin_value upload_max_filesize 16M
php_admin_flag session.upload_progress.enabled off

memory_limit = -1은 메모리 상한을 없애는 설정이다. 그래서 요청 크기 제한을 앞단 웹 서버에서 함께 걸어 두어야 메모리 고갈 위험을 줄일 수 있다. 권고는 디코딩된 요청 크기, 입력 필드 수, multipart 파트 수, 헤더 수, 중첩 깊이를 웹 서버나 프록시에서 제한하라고 한다. 프로세스에 RLIMIT_AS·RLIMIT_DATA 제한을 거는 방식은 오히려 취약한 할당 실패를 일으킬 수 있으니 쓰지 말라고 적었다. nginx라면 core 모듈 문서의 다음 지시어로 본문과 헤더 크기를 제한한다.

server {
    client_max_body_size 32m;          # 초과하면 413, 업로드가 없으면 더 낮게
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;  # 헤더 한 줄이 넘치면 400
}

PHP 쪽에서는 max_input_vars(기본 1000)와 max_input_nesting_level(기본 64)로 입력 변수 수와 중첩 깊이를 서비스에 필요한 만큼만 남긴다. 성능 여유가 있다면 GHSA가 제시한 pm.max_requests = 1도 고려할 수 있다. 워커가 요청 하나만 처리하고 교체되므로 이전 요청의 상태가 남지 않는다. 다만 매 요청마다 프로세스를 새로 띄우는 비용이 생긴다.

오류 출력을 숨기는 조치는 효과가 없다. 권고는 error_reporting, display_errors, @ 연산자로 경고를 가리는 것은 도움이 되지 않는다고 했다. 대신 요청 초기화 전에 경고를 내는 잘못된 설정을 지우고, 외부 요청으로 도달할 수 있는 치명 오류와 실행 시간 초과를 코드에서 없애라고 한다.

설정이 실제 워커에 적용됐는지 확인하기

설정을 바꾼 뒤에는 모든 워커를 재시작하고, CLI가 아니라 FPM·mod_php 워커에서 값을 확인한다. 로컬에서만 열리는 위치에 임시 확인 파일을 두고 결과를 본 뒤 바로 지운다.

<?php // ini-check.php — 확인 후 즉시 삭제
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, ' / ', PHP_SAPI, PHP_EOL;
foreach (['session.upload_progress.enabled', 'memory_limit', 'post_max_size',
          'upload_max_filesize', 'max_input_vars'] as $key) {
    echo $key, ' = ', var_export(ini_get($key), true), PHP_EOL;
}
location = /ini-check.php {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

curl -s http://127.0.0.1/ini-check.php로 확인했을 때 PHP_SAPI가 fpm-fcgi이고, session.upload_progress.enabled가 '0' 또는 ''로 나오면 워커에 적용된 것이다. 패치된 버전에서는 memory_limit의 동작도 바뀐다. PHP 매뉴얼에 따르면 8.5.12·8.4.27·8.3.36·8.2.35부터는 소프트 한도가 되어 할당이 일단 끝난 뒤 오류가 나고, 초과 폭은 memory_hard_limit_percent로 제한된다. 업데이트를 마치면 임시로 넣은 -1을 원래 값으로 되돌린다.

로그 점검과 자격 증명 교체

업데이트 전에 인터넷에 노출돼 있던 서버는 침해 여부를 확인한다. 공개일인 10월 21일보다 이전 기록까지 거슬러 본다. 큰 요청, HTTP 500, 웹 루트에 새로 생긴 PHP 파일이 우선 확인 대상이다. nginx 기본 combined 형식에는 요청 크기가 없으므로, 앞으로를 위해 $request_length를 로그 형식에 추가해 둔다.

# 회전된 .gz 로그까지 포함해 500 응답 수 확인 (combined 형식: 9번째 필드가 상태 코드)
zcat -f /var/log/nginx/access.log* | awk '$9 == 500' | wc -l

# 10월 21일 이후 웹 루트에 생기거나 바뀐 PHP 파일
find /var/www -name '*.php' -newermt '2026-10-21' -ls
log_format with_len '$remote_addr [$time_local] "$request" $status '
                    '$body_bytes_sent req_len=$request_length';
access_log /var/log/nginx/access.log with_len;

GHSA는 격리된 환경에서 error_reporting=E_ALL로 오류 로그를 남겨 살펴보라고 하면서, 로그가 깨끗하다고 안전이 증명되지는 않는다고 덧붙였다. 오류 로그에서 원인을 좁히는 방법은 에러 로그 디버깅 순서를 참고한다. 업데이트 전에 노출됐던 서버라면 흔적이 없더라도 DB 비밀번호, API 키, 관리자 비밀번호를 교체하는 편이 안전하다. 비밀값을 코드와 분리해 두는 방법은 시크릿·.env 노출 차단 설정에서 다룬다.

침해 흔적을 발견했을 때 지킬 순서

  1. 서버를 끄거나 재시작하지 않는다. 메모리에 남은 증거가 사라진다. 방화벽이나 로드밸런서에서 외부 접속만 막는다.
  2. KISA 보호나라에 침해사고를 신고하고, 메모리와 디스크 증거를 확보한다.
  3. 증거를 확보한 뒤 업데이트, 웹셸 제거, 자격 증명 교체 순으로 복구한다.

취약 여부를 확인하겠다고 출처를 알 수 없는 공개 PoC를 운영망에서 실행하지 않는다. KISA 공지도 이런 PoC에 악성코드가 섞여 있는 경우가 많다고 경고한다. 판단은 위의 버전 확인과 로그 점검으로 하고, 분석이 필요하면 업무망과 분리된 환경에서 한다.

초보자가 자주 실수하는 포인트

  • CLI에서 php -v가 최신으로 나와 안심했지만, 웹 요청은 다른 경로에 설치된 FPM이나 mod_php가 처리하고 있는 경우
  • Debian·Ubuntu 패키지의 버전 번호가 8.3.36보다 낮다는 이유로 이미 보안 패치가 들어간 패키지를 미패치로 판단하거나, 그 반대로 판단하는 경우
  • session.upload_progress.enabled를 애플리케이션 코드의 ini_set()이나 .user.ini로 끄고 적용됐다고 보는 경우
  • 의심 흔적을 발견하자마자 서버를 재시작하거나 꺼서 메모리에 남은 증거를 잃는 경우

체크리스트

  • 인터넷에 노출된 서버·컨테이너·외주 사이트마다 웹 요청을 처리하는 PHP의 버전과 SAPI를 기록했는가
  • 배포판 패키지라면 변경 이력에서 CVE-2026-109618·109619를 찾았는가
  • 업데이트 후 FPM·Apache를 재시작하고 워커에서 PHP_VERSION을 다시 확인했는가
  • 업데이트가 늦어지는 서버에 php_admin 계열 설정과 nginx 요청 크기 제한을 넣고 적용 여부를 확인했는가
  • 10월 21일 전후 로그와 웹 루트의 새 .php 파일을 점검했는가
  • 업데이트 전 노출됐던 서버의 DB 비밀번호·API 키·관리자 비밀번호 교체 계획이 있는가

자주 묻는 질문

PHP를 쓰는 상용 솔루션이나 장비도 확인해야 하나요?

그렇다. 제품 안에 PHP가 들어 있으면 서버의 PHP 패키지와 별개로 업데이트된다. NCSC-NL php2shell 저장소의 software 목록처럼 제품별 상태를 모은 자료가 있지만 전체 목록은 아니므로, 제조사 공지로 해결 버전을 확인한다.

PHP 8.6을 쓰고 있으면 어떻게 하나요?

php.net 권고에 따르면 8.6.0 RC4에 수정이 들어 있다. 정식 출시 전 RC를 운영에 쓰고 있다면 RC4 이상인지 확인한다.

패치 후에도 session.upload_progress를 꺼 두는 게 좋은가요?

GHSA-6wmf-4rv5-p3rp는 이 기능이 필요 없다면 패치 후에도 꺼 두라고 권한다. php.net은 10월 23일 별도 공지(CVE-2026-115560)에서도 다음 정기 버그 수정 릴리스까지 끄라고 안내했다. 업로드 진행률 표시를 쓰지 않는 서비스라면 끈 상태를 유지한다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.