주요 내용
- Werkzeug는 비밀번호 해싱, 파일명 정제, 안전한 경로 결합을 위한 보안 보조 도구와 PIN으로 보호되는 디버거를 제공합니다.
- 이 취약점의 CVE 내역은 디버거, 다중 파트 양식 구문 분석, 그리고 safe_join() 헬퍼, 그리고 쿠키 파싱을 중심으로 나타납니다.
- 3.1.x 브랜치만이 업스트림 보안 수정 사항을 보장받으며, 3.1.6 이상 버전에서는 이 문서에서 다루는 모든 CVE가 해결됩니다.
- TuxCare의 ELS pip를 통해 제공되는 Werkzeug 1.0.1, 2.2.3 및 2.3.8에 보안 수정 사항을 역포트합니다.
의존성 스캐너는 다음을 표시합니다 werkzeug 를 취약점으로 표시하지만, Flask가 자동으로 가져왔기 때문에 팀원 중 누구도 이를 설치한 기억이 없습니다. Werkzeug 는 Flask의 기반이 되는 툴킷으로, 모든 라우트 뒤에서 요청과 응답의 처리 과정을 담당합니다. 이 툴킷은 Flask가 위에 추가하는 기능과는 별개로, 자체적인 CVE 이력과 기본 설정을 가지고 있습니다.
이 글에서는 Werkzeug가 어떤 위협으로부터 보호해 주는지, 검증된 CVE가 어디에 위치하는지, 자신의 시스템이 해당 위협에 얼마나 노출되어 있는지 확인하는 방법, 그리고 운영 중인 Werkzeug 애플리케이션을 강화하는 방법에 대해 다룹니다.
Werkzeug이란 무엇이며, Flask 내에서 어떤 역할을 하는지
Werkzeug는 Pallets 프로젝트에서 유지 관리하는 WSGI 유틸리티 라이브러리입니다. 이 라이브러리는 HTTP 요청 및 응답의 저수준 메커니즘을 처리합니다. 즉, 헤더, 쿠키, 멀티파트 폼 본문 파싱, URL 경로 매칭, 그리고 로컬 개발을 위한 디버거 제공 등의 기능을 수행합니다.
Flask는 Werkzeug를 대체하는 것이 아니라, 이를 기반으로 구축되었습니다. Flask는 라우팅 데코레이터, 템플릿, 애플리케이션 구조를 제공하지만, 요청(request) 및 응답(response) 객체는 Werkzeug의 하위 클래스이며, 그 밑바탕에 있는 WSGI 메커니즘의 상당 부분도 Werkzeug에서 비롯됩니다. Flask를 설치하면 Werkzeug가 종속성으로 함께 설치되므로, 대부분의 팀은 의도적으로 requirements 파일에 Werkzeug를 추가하지 않고도 Flask를 실행합니다.
실제 운영 환경에서는 Gunicorn과 같은 WSGI 서버가 원시 HTTP를 파싱하여 Flask에 WSGI 환경을 전달합니다. 이 단계에서, 애플리케이션에 요청을 보낼 수 있는 사람이라면 누구나 Werkzeug의 요청 처리 과정에 존재하는 결함을 악용할 수 있습니다. 라우팅은 뷰 함수가 실행되기 전에 이루어지며, 그 외 대부분의 요청 데이터는 해당 데이터를 읽는 시점에 파싱됩니다. 예를 들어, 양식 파싱은 코드가 request.form 또는 request.files를 참조하는 순간 시작됩니다.
Werkzeug의 기본 보안 기능
Werkzeug는 Flask 애플리케이션이 별도의 의존성을 추가하지 않고도 사용할 수 있는 여러 보안 지원 기능과 보호 장치를 제공합니다. 이들 중 대부분은 애플리케이션이 실제로 호출해야만 효과를 발휘합니다.
werkzeug.security 는 generate_password_hash() 및 check_password_hash()함수를 제공하며, 이 함수들은 비밀번호의 솔트 처리 및 해시 생성을 애플리케이션 코드에 맡기지 않고 구성 가능한 알고리즘을 사용하여 수행합니다. secure_filename() 는 사용자가 제공한 파일 이름에서 경로 구분자 및 기타 안전하지 않은 문자를 제거하므로, 반환된 이름은 애플리케이션이 제어하는 디렉터리에 저장하기에 훨씬 안전합니다. 다만 이 함수는 이름만 확인합니다. 디스크에 이미 존재하는 파일은 확인하지 않으며, 빈 문자열을 반환할 수도 있으므로 업로드 코드는 여전히 두 가지 경우 모두를 처리해야 합니다.
safe_join() 함수는 디렉터리 수준에서도 동일한 이유로 존재합니다. 이 함수는 신뢰할 수 있는 기본 디렉터리와 사용자가 지정한 경로를 결합하고, 해당 기본 디렉터리를 벗어나는 경로는 반환하지 않도록 설계되었으며, 이는 send_from_directory() 가 파일을 안전하게 제공하기 위해 의존하는 기능입니다. 대화형 디버거는 브라우저에 실시간 파이썬 콘솔을 노출하기 전에 PIN 인증을 요구하는데, 이는 디버그 엔드포인트에 우연히 접속한 낯선 사람이 코드를 실행하는 것을 막기 위한 조치입니다.
이러한 보호 조치 중 어느 것도 그 자체만으로는 완벽하지 않습니다. 각 조치는 특정 위험을 줄여주며, 그중 두 가지인 safe_join() 과 디버거 PIN은, 바로 그 조치들이 해소하고자 했던 위험 영역에서 문서화된 취약점이 발견된 바 있습니다. 아래에 나열된 CVE 기록의 상당 부분이 바로 이러한 취약점에서 비롯된 것입니다.
Werkzeug 보안 위험 및 CVE
2026년 9월 현재, Werkzeug의 공개 CVE 기록은 2023년 이후 총 9건의 공개로 집계됩니다. 최근 공개된 CVE 중 일부는 CVSS v3.1과 CVSS v4.0 점수를 모두 가지고 있으나, 트래커에서는 두 점수가 항상 모두 표시되지는 않습니다. 어느 쪽 점수가 더 높은지는 CVE에 따라 다릅니다. safe_join() 클러스터는 v4.0에서 점수가 더 높은 반면, CVE-2024-49767은 v3.1에서 점수가 더 높습니다. 두 점수 중 하나만 확인하면 해당 취약점의 시급성이 과장되거나 과소평가될 수 있지만, 근본적인 버그 자체는 동일합니다.
| CVE | 위험도 (CVSS v3.1) | 영향을 받는 버전 | 수정됨 |
| 34069 | 7.5 높음 | < 3.0.3 | 3.0.3 |
| 46136 | 7.5 높음 (NVD) / 5.7 보통 (GitHub) | 2.0.0rc1–2.3.7, 3.0.0 | 2.3.8, 3.0.1 |
| 25577 | 7.5 높음 | < 2.2.3 | 2.2.3 |
| 49767 | 7.5 높음 | 2.0.0rc1–3.0.5 | 3.0.6 |
| 49766 | 5.3 중간 | <= 3.0.5 (Windows, Python < 3.11) | 3.0.6 |
| 66221 | 5.3 중간 | < 3.1.4 | 3.1.4 |
| 21860 | 5.3 중간 | < 3.1.5 | 3.1.5 |
| 27199 | 5.3 중간 | <= 3.1.5 | 3.1.6 |
| 23934 | 3.5 Low (NVD) / 2.6 Low (GitHub) | < 2.2.3 | 2.2.3 |
디버거 원격 코드 실행 취약점
디버거 문제인 CVE-2024-34069와 이에 대한 CSRF 악용 방식은 TuxCare의 다음 분석 자료에서 자세히 다루고 있습니다. 'Werkzeug의 지원 종료가 Python 애플리케이션에 미치는 영향'에서 자세히 다루고 있습니다. 간단히 말해, 디버거는 로컬호스트에만 바인딩되어 있고 PIN으로 보호되어 있더라도 코드 실행 위험을 초래할 수 있습니다. 이 공격을 수행하려면 공격자가 제어하는 도메인과 서브도메인, 해당 도메인과 상호작용하며 디버거 PIN을 입력하는 개발자, 그리고 디버거를 트리거하는 추측된 URL이 필요합니다. 이 모든 조건이 충족되면 공격자는 로컬호스트 전용 디버거에 접근하여 개발자의 컴퓨터에서 코드를 실행할 수 있습니다. 이 문제는 3.0.3 버전에서 업스트림에서 수정되었으나, 업스트림 2.x 버전에 대한 지원이 2.3.8에서 종료되었기 때문에 해당 수정 사항이 포함된 2.x 릴리스는 없습니다. 2.x 버전을 계속 사용 중인 팀의 경우, TuxCare의 ELS가 Werkzeug 2.3.8용 백포트된 수정 사항을 통해 이를 지원하며, CVE 트래커에는 지원되는 모든 버전에 대한 현재 지원 범위가 나열되어 있습니다.
다중 파트 파싱 서비스 거부 공격
동일한 코드 경로에서 세 가지의 별도 CVE가 발견되었습니다. CVE-2023-25577은 요청에 무제한의 멀티파트(multipart) 파트를 포함할 수 있게 하여, 공격자가 제어하는 파트 수에 비례하여 파싱 비용이 증가하다가 CPU나 메모리가 고갈될 때까지 지속되는 문제를 일으켰습니다. 이 문제는 2.2.3 버전에서 수정되었으며, 이 릴리스에서는 max_form_parts 제한이 도입된 릴리스이기도 합니다.
CVE-2023-46136은 동일한 파서를 대상으로 하지만 다른 접근 방식을 취했으며, 단 하나의 조작된 부분으로 구성됩니다. CR 또는 LF 문자로 시작하고, 그 뒤에 추가적인 CR 또는 LF 문자가 포함되지 않은 메가바이트 단위의 데이터가 이어지는 파일은 파서가 점점 커지는 버퍼를 계속 재스캔하여 경계를 찾도록 강요합니다. NVD는 이 취약점에 7.5(높음) 등급을, GitHub 권고문은 5.7(중간) 등급을 부여했습니다. 일부 추적 시스템에서는 8.0(높음)이라는 세 번째 수치를 보여주는데, 이는 현재의 GitHub 권고 사항이 아닌 공식 CVE 기록에 기재된 CNA 점수에서 비롯된 것입니다. 이러한 점수 차이에 대한 자세한 분석은 Werkzeug 지원 종료 관련 문서에 나와 있습니다.
CVE-2024-49767은 동일한 파서에서 다른 취약점을 해결했습니다. 특수하게 조작된 양식 본문은 max_form_memory_size 를 완전히 우회할 수 있었기 때문에, 애플리케이션이 설정한 메모리 제한이 실제로는 전혀 적용되지 않았습니다. NVD는 이 취약점에 7.5점(높음, CVSS v3.1)을, GitHub 권고문은 6.9점(중간, CVSS v4.0)을 부여했습니다.
파서 제한은 공격자가 제어하는 메모리 사용을 차단할 수 있을 만큼 충분히 일찍 적용될 때만 효과가 있으며, 적절한 제한 값은 공격 유형에 따라 달라집니다. max_form_parts 는 CVE-2023-25577과 같은 공격에서 발생하는 미세한 부분의 홍수를 제한합니다. max_content_length 및 리버스 프록시의 본문 크기 제한은 총 요청 크기를 제한하며, 이 두 가지 모두 멀티파트 파서가 내부적으로 문제를 포착하는 것에 의존하지 않습니다.
safe_join의 Windows 경로 관련 결함
safe_join() 는 약 16개월 동안 네 차례에 걸쳐 패치되었으며, 모든 수정 사항은 Windows에 특화된 것이었고, 모두 해당 함수가 경로의 안전성을 판단하는 방식과 관련된 것이었습니다.
CVE-2024-49766은 이 네 가지 중 가장 오래된 취약점입니다. Windows에서 실행되는 Python 3.11 이전 버전의 경우, os.path.isabs() 는 //server/share 과 같은 UNC 경로를 절대 경로로 인식하지 못하며, safe_join() 는 해당 검사에 의존하고 있으므로, UNC 경로가 필터링되지 않고 통과될 수 있습니다. 이 문제는 3.0.6 버전에서 수정되었습니다.
나머지 세 가지는 CON, AUX, NUL과 같은 Windows 예약 장치 이름과 관련이 있는데, Windows는 디렉터리의 실제 내용과 상관없이 모든 디렉터리에서 이러한 장치 이름이 존재하며 읽을 수 있는 것으로 간주합니다. CVE-2025-66221에 따르면 safe_join() 함수가 이러한 이름을 아무런 검증 없이 그대로 허용한다는 사실을 밝혀냈으며, 3.1.4 버전에서 이 문제가 수정되었습니다. CVE-2026-21860에서는 첫 번째 수정 사항이 CON.txt.html과 같은 복합 확장자, 끝에 공백이 붙은 이름, 그리고 몇 가지 추가적인 특수 이름을 놓쳤음을 밝혀냈으며, 이는 3.1.5 버전에서 해결되었습니다. 그 후 CVE-2026-27199는 example/NUL 과 같은 다중 세그먼트 경로가 여전히 통과된다는 사실이 밝혀졌으며, 3.1.6 버전에서 이 문제가 해결되었습니다.
이 세 가지 장치 이름의 경우 각각에서, send_from_directory() 는 디바이스를 성공적으로 열지만, 읽기 작업에서 무기한으로 멈춰 버려, 실제 트래픽을 처리할 워커가 더 이상 남아 있지 않을 때까지 요청당 하나의 워커를 점유하게 됩니다.
쿠키 파싱 취약점
CVE-2023-23934는 이 목록에서 가장 주목받지 못한 항목입니다. NVD는 이 취약점에 3.5(낮음) 등급을, CVE를 할당한 CNA인 GitHub는 2.6(낮음) 등급을 부여했습니다. 일부 브라우저는 다음과 같은 형식의 쿠키를 허용합니다. =value 와 같은 형식의 이름 없는 쿠키를 허용하며, 2.2.3 이전 버전의 Werkzeug는 다음과 같은 쿠키를 파싱했습니다. =__Host-test=bad 와 같은 쿠키를 유효한 __Host-test 쿠키로 해석했습니다.
따라서 악의적이거나 해킹당한 서브도메인이 해당 잘못된 형식의 쿠키를 심어두면, Werkzeug가 보호된 __Host-test 이름 아래에 공격자가 제어하는 값을 읽게 할 수 있습니다. 이는 __Host- 접두사의 본래 취지를 무력화시킬 수 있습니다. 이 문제는 2.2.3 버전에서 수정되었습니다.
두 점수 모두 낮은 이유는, 이 공격이 취약한 브라우저와 악성 또는 해킹당한 인접 서브도메인을 모두 필요로 하기 때문입니다. 위조된 쿠키에 세션 토큰이나 CSRF 토큰이 포함되어 있다면 그 결과는 또 다른 문제입니다. CVSS는 메커니즘 자체에 점수를 매길 뿐, 특정 애플리케이션이 해당 쿠키에 무엇을 저장하고 있는지는 알 수 없습니다.
Werkzeug 버전의 취약점을 확인하는 방법
노출 여부를 확인하는 데는 서비스당 약 2분이 소요됩니다. 여기서 목표는 단 한 가지 질문에 답하는 것입니다. 즉, Werkzeug 버전이 여전히 수정 패치를 받고 있는 브랜치에서 실제로 프로덕션 환경에서 실행되고 있는가 하는 점입니다.
요구 사항 문서에 명시된 내용이 아니라, 실제로 설치된 것부터 시작하십시오.
pip show werkzeug
실제로 트래픽을 처리하고 있는 컨테이너나 가상 환경 내에서 이 명령을 실행하십시오. 3.1보다 오래된 버전은 업스트림 보안 수정 사항이 보장되는 유일한 브랜치 외부에 위치합니다. Pallets는 요청에 따라 수정 사항을 이전 기능 브랜치로 백포트할 수 있지만, 이를 보장하지는 않습니다. 3.1.6보다 낮은 3.1.x 릴리스는 올바른 브랜치에 속해 있지만 여전히 적어도 하나의 수정 사항이 누락되어 있으며, 이 글을 작성하는 시점의 최신 릴리스는 3.1.8입니다. 어느 쪽이든, 위 표의 “영향을 받는 버전” 및 “수정된 버전” 열은 각 CVE가 적용되는지 여부와 해당 업스트림 수정 사항이 포함된 릴리스를 보여줍니다. 또한 Windows 전용인 safe_join() 행은 Windows 호스트에서만 중요합니다.
버전 번호만으로는 특정 파이썬 버전과 OS에 어떤 CVE가 적용되는지 판단할 수 없으므로, 해당 환경에서 감사 도구를 실행해야 합니다.
pip install pip-audit
pip-audit
인수가 없는 경우, pip-audit는 는 현재 활성화된 환경에 설치된 패키지를 확인합니다. 대신 요구 사항 파일을 감사해야 하는 경우, pip-audit -r requirements.txt 명령어를 사용하면 됩니다. pip-audit은 Python Packaging Authority에서 관리되며, 기본적으로 PyPI에서 취약점 데이터를 가져오지만, -s osv를 통해 OSV를 대체 소스로 사용할 수 있습니다. 일부 팀은 자문 데이터에 대한 또 다른 관점을 얻기 위해 Safety와 같은 두 번째 스캐너를 함께 실행하기도 합니다.
디스크에 있는 패키지를 업데이트해도 실행 중인 워커가 이미 임포트한 Werkzeug 코드는 대체되지 않습니다. 장시간 실행되는 Gunicorn 또는 uWSGI 워커는 재시작할 때까지 기존 코드를 계속 로드한 상태로 유지하므로, Werkzeug을 변경한 후에는 워커를 재시작하고 새 버전이 단순히 설치된 것이 아니라 실제로 로드되었는지 확인해야 합니다.
특히 Werkzeug 보안 뉴스의 경우, TuxCare의 CVE 트래커는 는 더 이상 업스트림 수정 패치를 제공받지 못하는 구버전에서 확인된 ELS 패치 적용 취약점을 나열합니다.
Werkzeug 애플리케이션 보안을 위한 모범 사례
Werkzeug의 위험 요소 중 일부는 버전이 아니라 구성에서 비롯되며, 이러한 관행은 모든 릴리스에 적용됩니다.
디버거부터 시작해 봅시다. 다음을 설정하세요. debug=False 로 설정하고, 프로덕션 환경에서는 127.0.0.1에 바인딩된 경우라도 절대 활성화하지 마십시오. CVE-2024-34069 사례에서 알 수 있듯이, localhost는 겉보기와 달리 안전한 경계가 아니기 때문입니다. Gunicorn, uWSGI, Waitress와 같은 프로덕션용 WSGI 서버 뒤에서 실행하십시오. 왜냐하면 run_simple() 와 flask run 는 개발용 도구로, 프로덕션 트래픽을 처리하도록 설계된 적이 없습니다.
다음으로, 의도적으로 요청 제한을 설정하세요. max_content_length, max_form_memory_size, 그리고 max_form_parts 는 애플리케이션이 실제로 허용하는 값과 일치해야 하며, 리버스 프록시에서도 본문 크기를 제한해야 합니다. 그래야만 지나치게 크거나 복잡한 멀티파트 요청이 메모리나 CPU를 심각하게 소모하기 전에 차단될 수 있습니다. 명시적인 값 설정은 3.1 이전 버전에서 가장 중요하며, 이 버전에서는 max_form_memory_size 의 기본값이 무제한으로 설정되어 있었습니다. Werkzeug 3.1에서는 이 기본값이 500 kB로 변경되었습니다.
가능한 경우 nginx와 같은 전용 웹 서버를 통해 정적 파일을 제공하십시오. 이렇게 하면 send_from_directory() 및 safe_join() 가 해당 요청 경로에서 완전히 배제되도록 하십시오. 또한 애플리케이션 프로세스를 루트 권한이 없는 사용자로 실행하여, 설령 경로 관련 버그가 발생하더라도 접근 가능한 범위가 좁아지도록 하십시오.
설정을 통해 버전 업그레이드만으로는 해결되지 않는 취약점을 보완할 수 있으며, 그 반대의 경우도 마찬가지입니다. 이러한 설정 중 어느 것도 최신 3.1.x 릴리스를 실행하는 것을 대체할 수는 없으며, 위 표에 나열된 모든 CVE는 3.1.6 및 이후 버전에서 수정되었습니다.
TuxCare ELS가 지원 종료 후에도 Werkzeug의 보안을 유지하는 방법
정해진 일정에 따라 항상 업그레이드가 가능한 것은 아닙니다. 공급업체에서 제공한 빌드, 인증된 배포 환경, 또는 이전을 위한 예산이 배정되지 않은 Python 런타임 등으로 인해, 해당 표에 나열된 CVE가 애플리케이션에 적용되기 시작한 지 한참이 지난 후에도 애플리케이션이 오래된 Werkzeug 릴리스에 묶여 있을 수 있습니다.
Werkzeug에 대한 TuxCare의 무제한 라이프사이클 지원 버전 1.0.1, 2.2.3 및 2.3.8에 대한 보안 패치를 계속 제공하며, 이는 pip를 통해 서명된 패키지 형태로 배포됩니다. 팀은 이미 사용 중인 버전을 유지한 채로 보안 패치를 지속적으로 받을 수 있으므로, 새로운 CVE가 발생한다고 해서 자동으로 긴급 마이그레이션을 진행해야 하는 것은 아니며, 실제 마이그레이션은 팀이 정한 일정에 따라 진행할 수 있습니다.
Werkzeug 보안에 관한 자주 묻는 질문
사용 중인 Werkzeug 버전에 알려진 취약점이 있는지 어떻게 확인할 수 있나요?
실행 pip show werkzeug 를 실행하여 설치된 버전을 확인한 후, pip-audit 를 실행하여 패키지가 알려진 보안 권고 사항에 부합하는지 확인하십시오. 여기서 다루는 CVE의 경우, 3.1.6이 모든 수정 사항이 포함된 최초의 업스트림 릴리스이지만, 작성 시점 기준 3.1.8이 최신 릴리스이며 더 적합한 대상입니다.
Flask를 업그레이드하면 Werkzeug의 CVE 문제도 해결되나요?
꼭 그렇지는 않습니다. Flask는 Werkzeug의 상한값이 아닌 최소 버전을 지정하며, pip install –upgrade flask 명령어는 이미 설치된 Werkzeug가 해당 최소 버전을 충족하는 한 그대로 둡니다. Flask를 업그레이드하는 것은 권장되지만, 정확히 확인하려면 해결된 Werkzeug 버전을 직접 확인하는 것만이 유일한 방법입니다.
프로덕션 환경에서 Werkzeug를 실행하는 가장 안전한 방법은 무엇인가요?
Gunicorn, uWSGI, Waitress와 같은 전용 WSGI 서버에서 다음 옵션을 사용하여 실행하십시오. debug=False를 설정하고, 명시적인 max_content_length, max_form_memory_size, 그리고 max_form_parts 제한 사항, 그리고 send_from_directory()를 사용하는 대신 별도의 웹 서버에서 제공하는 정적 파일.
왜 일부 Werkzeug CVE의 심각도 점수가 장소에 따라 다르게 표시되나요?
최근 발견된 몇 가지 Werkzeug CVE에는 CVSS v3.1 점수와 CVSS v4.0 점수가 모두 포함되어 있으며, 두 프레임워크는 동일한 세부 사항을 서로 다르게 평가합니다. CVE-2026-27199는 safe_join() 수정 사항 중 하나인 CVE-2026-27199는 NVD에서는 CVSS v3.1 기준으로 5.3, GitHub 권고문에서는 CVSS v4.0 기준으로 6.3을 기록하고 있으므로, 트래커에 표시되는 수치는 해당 트래커가 기본으로 설정된 출처와 표준에 따라 달라집니다. 그러나 이 취약점은 어느 쪽을 기준으로 하더라도 동일한 양상을 보입니다.

