
보안 권고: dash-uploader(Python/PyPI)의 CVE-2026-38360 경로 탐색(CWE-22)
인증되지 않은 사용자가 fohrloop/dash-uploader(Python, PyPI)에서 임의의 파일을 쓸 수 있게 하는 경로 탐색 취약점으로, (이에 국한되지 않고) 원격 코드 실행(RCE), 애플리케이션 소스 코드 덮어쓰기, 저장형 XSS, 지속적 백도어 설치로 이어질 수 있습니다.
이 저장소는 활성 유지 관리자 없이 2025-07-19에 보관(archived)되었습니다. 공개된 모든 버전(0.1.0부터 0.7.0a2까지)이 영향을 받으며 앞으로도 그 상태로 남을 것입니다. 이 패키지는 여전히 월 약 28,000건의 다운로드를 기록하고 있습니다.
프로덕션 환경에서 dash-uploader를 실행하는 모든 사용자는 스스로 완화 조치를 적용해야 합니다. 권장 수정 방법은 Plotly Dash에 내장된 dcc.Upload 컴포넌트로 마이그레이션하는 것입니다. 전체 옵션은 완화(Mitigation)를 참조하세요.
| CVE ID | CVE-2026-38360 (NVD) |
| 취약점 유형 | 경로 탐색 (CWE-22) |
| CVSS 3.1 | 9.8 / 치명적 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| 제품 | dash-uploader |
| 영향받는 버전 | 0.1.0부터 0.7.0a2까지 (총 18개 릴리스) |
| 수정 버전 | 없음 (프로젝트는 2025-07-19에 보관됨) |
| 공격 벡터 | 원격, 인증 불필요 |
| 발견자 | Muhammad Fitri Bin Mohd Sultan |
| 배정 기관 | MITRE, 2026-05-07 |
| 관련 CVE | CVE-2026-38361 (동일 라이브러리의 DoS) |
dash_uploader/httprequesthandler.py의 request.form.get()에서 가져온 세 개의 사용자 제어 매개변수가 검증이나 정화(sanitization) 없이 os.path.join()과 os.makedirs()에 직접 전달됩니다:
upload_id (57행 → 161행, BaseHttpRequestHandler.get_temp_root): 대상 디렉터리를 제어합니다. 공격자는 upload_id=../../../../usr/local/lib/python3.10/site-packages를 보내 파일이 Python의 패키지 디렉터리에 기록되게 할 수 있습니다.
resumableFilename (51행 → 108행, BaseHttpRequestHandler._post): 최종 파일명을 제어합니다. 공격자는 정상적인 upload_id가 있어도 파일명을 통해 업로드 디렉터리를 벗어날 수 있습니다.
resumableIdentifier (54행 → 64행, BaseHttpRequestHandler._post): os.makedirs()와 함께 사용되어 임시 디렉터리를 생성합니다. 공격자는 파일시스템 어디에나 임의의 디렉터리를 만들 수 있습니다.
업로드 엔드포인트(기본값 /API/dash-uploader)는 인증을 요구하지 않습니다. v0.5.0에서 추가된 http_request_handler 훅은 post_before()를 통한 요청 전 검사를 허용하지만, 취약한 _post() 메서드는 훅이 반환된 후 모든 매개변수를 request.form에서 직접 읽습니다. 훅은 라이브러리가 매개변수를 처리하기 전에 이를 정화할 수 없습니다. 훅을 통해 인증을 추가한 개발자도 인증된 사용자의 경로 탐색 공격에는 여전히 취약합니다.
# dash_uploader/httprequesthandler.py
def _post(self):
resumableFilename = request.form.get("resumableFilename", default="error", type=str)
resumableIdentifier = request.form.get("resumableIdentifier", default="error", type=str)
upload_id = request.form.get("upload_id", default="", type=str)
...
temp_root = self.get_temp_root(upload_id) # upload_id flows in here
temp_dir = os.path.join(temp_root, resumableIdentifier) # raw user input -> os.path.join
if not os.path.isdir(temp_dir):
os.makedirs(temp_dir) # raw user input -> os.makedirs
def get_temp_root(self, upload_id):
return os.path.join(self.upload_folder, upload_id) # no sanitization: ../../ escapes upload_folder
세 개의 독립적인 탐색 싱크(sink)는 동일한 근본 원인을 공유합니다. 폼 값이 검증 없이 os.path.join과 os.makedirs에 도달합니다.
인증되지 않은 원격 공격자는 업로드 엔드포인트에 HTTP POST multipart 요청을 보냅니다. upload_id 폼 매개변수에 경로 탐색 시퀀스(../)를 주입함으로써 공격자는 업로드된 파일의 대상 디렉터리를 제어합니다. 예를 들어 upload_id=../../../../usr/local/lib/python3.10/site-packages를 보내면 Python의 패키지 디렉터리에 파일이 기록되고, 다음 인터프리터 시작 시 .pth 자동 실행을 통해 RCE가 가능해집니다.
인증, 세션 토큰, CSRF 토큰이 필요하지 않습니다. 추가 매개변수 두 개(resumableFilename, resumableIdentifier)는 동일한 엔드포인트를 통해 독립적인 경로 탐색 벡터를 제공합니다. 공식 퀵스타트 문서에 나와 있는 기본 라이브러리 구성은 단일 curl 명령으로 악용될 수 있습니다.
서버 프로세스가 쓸 수 있는 모든 디렉터리에 임의의 파일을 쓸 수 있습니다. 이는 다음과 같은 잘 알려진 프리미티브를 통해 **원격 코드 실행(RCE)**으로 이어집니다:
RCE 프리미티브
.pth 파일을 site-packages에 넣습니다. 다음 인터프리터 시작 시 공격자가 제공한 코드를 실행합니다.sitecustomize.py 또는 usercustomize.py 주입. 모든 Python 시작 시 실행됩니다.app.wsgi, wsgi.py) 덮어쓰기. 다음 워커 리로드 시 실행됩니다./etc/cron.d/, /etc/cron.hourly/, 사용자 crontab 스풀). 예약 실행됩니다./etc/systemd/system/, ~/.config/systemd/user/). 다음 서비스 시작 또는 재부팅 시 실행됩니다./etc/ld.so.preload 주입. 이후 모든 바이너리 실행에 공격자 코드를 프리로드합니다.~/.bashrc, ~/.profile, ~/.bash_profile). 앱 사용자의 다음 인터랙티브 로그인 시 실행됩니다.~/.ssh/authorized_keys에 추가. 앱 사용자 권한으로 호스트에 대한 지속적인 SSH 접근을 부여합니다.웹 계층 영향
site-packages의 Dash 프레임워크 JS)를 덮어써 호스트 도메인에서 저장형 크로스사이트 스크립팅을 유발하며, 애플리케이션이 재시작될 때까지 모든 페이지 로드에서 모든 사용자에게 영향을 줍니다파일시스템 영향
resumableIdentifier 매개변수를 사용한 os.makedirs()를 통해 프로세스가 접근할 수 있는 모든 곳에 임의의 디렉터리 생성(inode 고갈 또는 존재하지 않는 디렉터리 트리에 쓰기를 준비하는 데 사용 가능)dash_uploader/httprequesthandler.pyBaseHttpRequestHandler.get_temp_root()BaseHttpRequestHandler._post()현재 배포 중인 사용자를 위한 옵션은 선호 순서대로 다음과 같습니다:
dcc.Upload로 마이그레이션 — Plotly Dash와 함께 제공되는 공식 업로드 컴포넌트입니다. 파일은 base64 문자열로 콜백에 도착하며, 파일시스템에 쓰는 핸들러가 노출되지 않고 클라이언트가 제어하는 대상 경로도 없으므로 이 버그 유형은 적용되지 않습니다. 중소형 파일에 가장 적합합니다. 매우 큰 업로드의 경우 항목 2를 참조하세요.
소규모 Flask 업로드 핸들러를 직접 작성 — werkzeug.utils.secure_filename()과 하드코딩된 서버 측 대상 디렉터리를 사용하세요. 클라이언트가 제공한 upload_id, 파일명, 식별자 값을 경로 구성 요소로 절대 허용하지 마세요.
dash-uploader를 계속 사용해야 한다면 업로드 엔드포인트를 인증 뒤에 배치하고, 라이브러리 핸들러가 요청을 보기 전에 요청을 재작성하거나 거부하는 계층에서 upload_id, resumableFilename, resumableIdentifier를 엄격한 허용 목록(예: UUID만)으로 검증하세요. 라이브러리의 http_request_handler 훅은 훅이 반환된 후 매개변수가 request.form에서 읽히기 때문에 경로 탐색을 막지 못합니다. 정화(sanitization)는 라이브러리 위 계층에서 수행되어야 합니다.
리버스 프록시 또는 WAF 계층에서 업로드 엔드포인트에 대한 요청 중 어떤 폼 필드에 .., 인코딩된 변형(%2e%2e, ..%2f, %2e%2e%2f) 또는 절대 경로가 포함된 요청을 거부하세요.
| 날짜 | 이벤트 |
|---|---|
| 2026-03-17 | 프로덕션 배포 환경에 대한 보안 연구 중 취약점 발견. |
| 2026-03-19 | MITRE에 CVE 요청 제출. |
| 2026-05-07 | MITRE가 CVE-2026-38360 배정. |
| 2026-05-07 | 공개 권고 게시. |
| 2026-05-09 | CVE 레코드가 MITRE CVE 데이터베이스와 NVD에 게시됨. |
0.6.1(안정 버전 라인). 사전 릴리스는 0.7.0a2까지 있습니다.dash. 선택 의존성: pyyaml. 라이선스: MIT.Muhammad Fitri Bin Mohd Sultan