PHP 원격 코드 실행 취약점 PHP2Shell(CVE-2026-109618, CVE-2026-109619)에 대응하려고 도커 컨테이너를 업데이트하다가 SVN 관리 기능이 동작하지 않아, PHP 없이 Go로 SVN 서버·웹 관리 도구 SvnGo를 만든 과정과 사용법을 정리했습니다.

myapps Apache Docker Go PHP PHP2Shell Subversion SVN SvnGo 보안 시놀로지

지난주에 PHP 엔진 자체에서 원격 코드 실행 취약점이 공개되었습니다. 이름은 PHP2Shell이고, CVE 번호는 CVE-2026-109618과 CVE-2026-109619 두 개입니다.

집 NAS에서 도커로 돌리던 서비스들을 점검하고 컨테이너를 업데이트하다 보니, 오래 써 온 Subversion(SVN) 서버의 관리 기능이 제대로 동작하지 않았습니다. 고쳐 쓰는 대신 PHP를 아예 걷어 낸 SVN 서버와 웹 관리 도구를 새로 만들었고, 이름은 SvnGo로 붙였습니다. 만든 계기와 SvnGo 소개, 도커 이미지 사용법을 정리해 둡니다.

PHP2Shell(CVE-2026-109618, CVE-2026-109619)

2026년 10월 21일 PHP 보안팀이 두 취약점과 패치 버전을 함께 공개했습니다. 요약하면 다음과 같습니다.

항목 내용
CVE-2026-109618 메모리 부족(OOM) 처리 중 요청 데이터가 반쯤 초기화된 상태로 남고, 이후 정리 과정이 이 데이터를 정상 값으로 취급하는 문제
CVE-2026-109619 요청이 쓰던 내부 상태가 메모리 해제 후에도 남아, 같은 워커가 처리하는 다음 요청이 이를 사용하는 문제(use-after-free)
심각도 둘 다 CVSS v4.0 9.2 (Critical), 인증 없이 원격 코드 실행 가능
영향 범위 PHP 5.4부터 8.5.11까지
패치 버전 8.5.12, 8.4.27, 8.3.36, 8.2.35
구버전 8.1 이하는 공식 릴리스 없음 (7.4~8.1은 소스 백포트만 제공)

웹 요청을 처리하는 PHP라면 PHP-FPM, Apache mod_php, php-cgi 등 실행 방식과 관계없이 영향을 받습니다. 특히 mod_php는 애플리케이션 코드가 실행되기도 전에, 로그인 없이 보낸 요청 하나로 공격이 가능하다고 합니다. 로그인 화면이나 CSRF 검사, 앱 안의 보안 장치로는 막을 수 없다는 뜻입니다. 공개 당일부터 실제 공격 시도가 관찰되었고, 10월 26일에는 미국 CISA의 KEV(실제 악용 중인 취약점) 목록에도 올라갔습니다.

자세한 내용은 아래 글을 참고하세요.

컨테이너를 업데이트하다가 SVN 관리가 막힘

취약점 소식을 보고 NAS에서 돌리던 도커 컨테이너를 하나씩 점검했습니다. PHP가 들어 있는 컨테이너는 정리하거나 패치된 버전으로 올렸고, SVN 서버 컨테이너도 같이 손을 봤습니다. 이 컨테이너는 몇 년 전에 받아 둔 이미지라 Apache도 2015년 버전(2.4.17)이었고, 저장소·사용자·권한 관리를 PHP로 만든 웹 관리 도구로 하고 있었습니다.

그런데 컨테이너를 업데이트하고 나니 SVN 관리 기능이 제대로 동작하지 않았습니다. 체크아웃·커밋 같은 SVN 자체는 살아 있었지만, 저장소를 만들거나 사용자·권한을 바꾸는 웹 관리 화면을 쓸 수 없게 된 것입니다.

선택지는 세 가지 정도였습니다.

  1. 관리 도구를 고쳐서 최신 PHP에 맞추기
  2. 다른 PHP 기반 SVN 관리 도구 찾기
  3. PHP 없이 새로 만들기

1번과 2번은 결국 PHP를 계속 들고 가는 방법이라, 다음 PHP 취약점이 나오면 같은 일을 또 해야 합니다. SVN 관리 도구에 필요한 기능은 저장소 생성, 사용자·그룹, 경로별 권한, 백업 정도라 크지 않습니다. 요즘은 AI 코딩 도구로 이 정도 규모는 빠르게 만들 수 있어서, 3번으로 정했습니다.

SvnGo 소개

SvnGo는 Go로 작성한 경량 SVN 서버 겸 웹 관리 시스템입니다. 도커 컨테이너 하나에 모든 구성 요소가 들어 있습니다.

  • SVN 프로토콜은 검증된 Apache httpd + mod_dav_svn이 처리합니다. SVN 저장소 형식이나 프로토콜을 새로 구현하지 않았습니다.
  • 저장소·사용자·그룹·권한 관리는 Go 관리 서버가 맡습니다.
  • PHP, Node.js, Java, Redis, ORM, 프론트엔드 프레임워크를 쓰지 않습니다. 화면은 Go html/template과 HTMX로 만들었습니다.
  • 이미지 크기는 Docker Hub 압축 기준 약 15MB이고, 유휴 상태 메모리는 Apache를 포함한 컨테이너 전체가 약 12MB 수준입니다.

구조는 대략 이렇습니다.

svn 클라이언트 / 웹 브라우저
        │ HTTP(S)
        ▼
Apache httpd (:8080 / :8443)
  ├─ /svn/<저장소>  mod_dav_svn + mod_authz_svn (htpasswd, authz)
  └─ /admin        리버스 프록시 → Go 관리 서버 (127.0.0.1:9000)
                                    ├─ SQLite (사용자·권한 원본 데이터)
                                    ├─ htpasswd / authz 파일 생성·검증·교체
                                    └─ svnadmin / svnlook / svn 실행

사용자·그룹·권한 정보는 SQLite에만 저장하고, 바뀔 때마다 Apache가 읽는 htpasswd와 authz 파일을 다시 만듭니다. 새 파일은 임시 파일로 먼저 만들어 libsvn으로 검증하고, 문제가 없을 때만 기존 파일과 한 번에 바꿔치기합니다. 그래서 교체 도중에 Apache가 반쯤 쓰인 파일을 읽는 일이 없고, 실패하면 이전 파일로 되돌립니다. Apache는 요청마다 이 파일을 읽기 때문에 사용자나 권한을 바꿔도 Apache를 재시작할 필요가 없습니다.

주요 기능

  • 대시보드: 저장소·사용자·그룹·권한 수, Apache 상태, 디스크 사용량, 최근 작업과 변경 이력
  • 저장소: 생성(trunk/branches/tags 선택), 이름 변경, 삭제, 활성/비활성, 검증(svnadmin verify), 백업·복원
  • 웹 브라우징: 디렉터리·파일 보기, 커밋 로그, 변경 파일, diff, 원본 다운로드
  • 사용자·그룹: 생성·수정·삭제, 비밀번호 변경, 임시 비밀번호 발급, 기존 htpasswd 가져오기
  • 권한: 저장소·디렉터리 단위로 읽기 / 읽기쓰기 / 접근 금지 설정, 저장 후 libsvn으로 실제 적용 결과 확인
  • 백업: svnadmin dump 결과를 gzip으로 저장, 새 저장소로 복원하거나 덮어쓰기 복원
  • 로그: 감사 로그(누가 무엇을 바꿨는지), 애플리케이션 로그, Apache 로그
  • 다국어: 한국어·영어 기본 제공, 브라우저 언어 자동 선택, 언어 파일(JSON)로 다른 언어 추가
  • 접근 IP 제한: 관리 화면과 SVN 각각 허용 IP/CIDR 목록 지정

화면 둘러보기

샘플 데이터(저장소 5개, 사용자 7명, 그룹 4개)를 넣고 캡처한 화면입니다.

대시보드 – 저장소·사용자·그룹·권한 수와 서버 상태, 최근 작업, 변경 이력을 한 화면에서 봅니다.

SvnGo 대시보드

저장소 개요 – 체크아웃 URL과 UUID·리비전·크기, 최근 커밋이 나옵니다. 체크아웃 명령은 복사 버튼으로 바로 가져갈 수 있습니다.

SvnGo 저장소 개요

커밋 상세와 diff – 리비전별 작성자·메시지, 변경 파일과 파일별 diff를 웹에서 확인합니다.

SvnGo 커밋 상세와 diff

저장소 권한 – 경로별 권한 규칙을 관리합니다. 아래쪽 권한 확인 도구에 사용자와 경로를 넣으면, SvnGo가 계산한 결과와 libsvn이 실제로 판단한 결과를 함께 보여 줍니다.

SvnGo 저장소 권한

시스템 설정 – 버전, Apache·Subversion 상태, 인증 파일 동기화 검증, SVN 접속 차단 목록, 서버 설정을 관리합니다.

SvnGo 시스템 설정

SVN 사용자로 로그인한 화면 – SVN 사용자도 웹에 로그인할 수 있지만, 권한이 있는 저장소와 경로만 보이고 관리 메뉴는 나오지 않습니다.

SVN 사용자로 로그인한 SvnGo 화면

다크 모드는 운영체제 설정을 따릅니다. 나머지 화면(로그인, 사용자·그룹, 백업, 로그 등)은 GitHub README에 있습니다.

보안 쪽에서 신경 쓴 부분

이번 일의 출발이 취약점이었기 때문에 보안을 가장 신경 써서 만들었습니다.

  • 런타임 최소화: PHP 같은 스크립트 런타임이 없어서, 이번 PHP2Shell 같은 취약점과는 관계가 없습니다.
  • 명령 실행: 셸을 거치지 않고 고정 인자로 svnadmin 등을 실행합니다. 경로와 이름은 검증한 뒤 넘기고, 저장소 밖 경로로는 접근할 수 없습니다.
  • 웹 보안: 템플릿 자동 이스케이프와 엄격한 CSP(인라인 스크립트 금지), 여러 겹의 CSRF 방어, SQL 파라미터 바인딩을 적용했습니다.
  • 비밀번호: bcrypt로만 저장하고 로그에 남기지 않습니다. 웹 로그인 실패 횟수를 제한합니다.
  • SVN 무차별 대입 차단: SVN 로그인에 15분 안에 20번 실패한 IP는 15분 동안 차단합니다. Apache가 인증보다 먼저 거부하기 때문에 차단된 동안에는 비밀번호를 더 시도할 수 없습니다.
  • 접근 IP 제한: 관리 화면과 SVN에 접속할 수 있는 IP 대역을 따로 정할 수 있습니다.
  • 컨테이너: non-root(uid 1000)로 실행하고 읽기 전용 루트 파일시스템, cap_drop: ALL, no-new-privileges를 기본으로 적용합니다. 혹시 코드가 실행되더라도 실행 파일이나 Apache 설정을 바꿀 수 없습니다.

Docker Hub 이미지

소스를 빌드하지 않아도 Docker Hub에 올려 둔 이미지를 바로 받아 쓸 수 있습니다.

태그 플랫폼 설명
1.0.1 linux/amd64 최신 버전. 다국어와 접근 IP 제한 추가. 운영 환경에서는 이렇게 버전을 고정해서 쓰는 것을 권장
latest linux/amd64 가장 최근 릴리스 (현재 1.0.1과 같은 이미지)
1.0.0 linux/amd64 첫 릴리스

Docker Hub 이미지는 linux/amd64용입니다. Raspberry Pi나 일부 시놀로지 모델처럼 ARM CPU를 쓰는 호스트에서는 GitHub 소스로 arm64 이미지를 직접 빌드해야 합니다.

docker run으로 실행 (Windows 11 PowerShell)

docker run -d --name svngo --restart unless-stopped `
  -p 80:8080 -p 443:8443 `
  -e ADMIN_PASSWORD="Change-Me-123" `
  -v svn-repos:/data/repos -v svn-config:/data/config `
  -v svn-db:/data/db -v svn-backups:/data/backups `
  --read-only `
  --tmpfs "/run/svngo:size=16m,mode=0750,uid=1000,gid=1000" `
  --tmpfs "/tmp:size=64m,mode=1777" `
  --cap-drop ALL `
  --security-opt "no-new-privileges:true" `
  isul/svngo:1.0.1

데이터는 볼륨 네 개(repos, config, db, backups)에 저장되므로, 새 버전으로 컨테이너를 다시 만들어도 그대로 유지됩니다.

docker compose로 실행

services:
  svngo:
    image: isul/svngo:1.0.1
    container_name: svngo
    restart: unless-stopped
    ports:
      - "80:8080"
      - "443:8443"
    environment:
      ADMIN_USERNAME: admin
      ADMIN_PASSWORD: Change-Me-123   # 8자 이상, 처음 실행할 때만 사용
      SVNGO_SERVER_NAME: svn.example.com
      TZ: Asia/Seoul
    volumes:
      - svn-repos:/data/repos
      - svn-config:/data/config
      - svn-db:/data/db
      - svn-backups:/data/backups
    read_only: true
    tmpfs:
      - /run/svngo:size=16m,mode=0750,uid=1000,gid=1000
      - /tmp:size=64m,mode=1777
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    stop_grace_period: 30s

volumes:
  svn-repos:
  svn-config:
  svn-db:
  svn-backups:
docker compose pull
docker compose up -d
docker compose ps   # STATUS가 healthy이면 준비 완료

접속 주소

용도 주소
웹 관리 화면 http://<서버>/admin/
SVN 저장소 http://<서버>/svn/<저장소명>
HTTPS (인증서 설정 시) https://<서버>/admin/, https://<서버>/svn/<저장소명>

처음 실행할 때 ADMIN_USERNAME / ADMIN_PASSWORD로 관리자 계정을 만듭니다. 로그인한 뒤 바로 비밀번호를 바꾸는 것이 좋습니다. 웹 화면에서 저장소와 사용자를 만들고 권한을 준 다음, TortoiseSVN이나 svn 명령으로 평소처럼 사용하면 됩니다.

svn checkout http://localhost/svn/myrepo/trunk myrepo --username kim

기존 SVN 서버에서 옮기기

기존 SVN 서버를 쓰고 있었다면 저장소와 계정을 그대로 옮길 수 있습니다.

  1. 저장소: 기존 저장소 폴더를 repos 볼륨에 복사하면 시작할 때 자동으로 등록됩니다. svnadmin dump 파일이 있다면 백업 화면에서 올려서 새 저장소로 복원할 수도 있습니다.
  2. 사용자: 사용자 화면의 htpasswd 가져오기로 기존 htpasswd를 옮깁니다. bcrypt와 APR1-MD5 해시를 지원하므로 사용자는 기존 비밀번호로 그대로 접속할 수 있습니다.
  3. 권한: 그룹과 권한 규칙은 웹 화면에서 다시 설정합니다. authz 파일은 DB에서 자동으로 만들어지므로 직접 수정하지 않습니다.

시놀로지 NAS의 Container Manager에서 쓰는 방법(폴더 권한 준비, 포트, DSM 인증서로 HTTPS 켜기)도 README의 시놀로지 항목에 정리해 두었습니다. 시놀로지에서는 데이터 폴더 소유자를 uid/gid 1000으로 맞춰야 컨테이너가 시작됩니다.

제한 사항

  • svn://(svnserve)는 지원하지 않고 HTTP/HTTPS만 지원합니다.
  • 인증은 htpasswd(Basic 인증)만 지원합니다. LDAP/AD 연동은 없습니다.
  • 컨테이너 하나로 동작하는 구조라 여러 인스턴스로 늘리는 수평 확장은 지원하지 않습니다.
  • Docker Hub 이미지는 linux/amd64만 있습니다.

마치며

PHP2Shell은 PHP를 쓰는 서비스라면 대부분 해당되는 취약점이라, PHP가 들어간 컨테이너는 패치된 버전으로 올리거나 정리하는 것이 좋습니다. 저는 그 과정에서 멈춘 SVN 관리 도구를 고치는 대신 PHP를 아예 걷어 내는 쪽을 택했고, 그렇게 나온 결과물이 SvnGo입니다.

혹시 오래된 PHP 기반 SVN 관리 도구를 아직 쓰고 계시거나, 가볍게 띄울 수 있는 SVN 서버가 필요하다면 isul/svngo 이미지를 한번 써 보세요. 문제나 개선 의견은 GitHub에 남겨 주시면 됩니다.