Notepad++ session.xml backupFilePath 경로 순회 취약점
개요
Notepad++가 세션을 복원할 때 session.xml의 backupFilePath를 정규화하지 않은 채 문자열 접두사만 검사해, 백업 디렉터리 밖의 임의 파일을 읽을 수 있는 취약점을 발견했습니다.
공격자가 session.xml에 백업 디렉터리로 시작하면서 ..\가 포함된 경로를 삽입하면 std::wstring::starts_with() 검사를 통과합니다. 이후 Windows가 실제 파일을 열 때 상위 디렉터리 참조를 해석하면서 사용자 권한으로 읽을 수 있는 파일이 Notepad++ 편집기 탭에 로드됩니다.
| 항목 | 내용 |
|---|---|
| 영향 버전 | v8.9.7 미만 (CVE 레코드 기준, v8.9.6.2에서 검증) |
| 패치 버전 | v8.9.7 |
| 심각도 | Medium — CVSS v4.0 5.1 |
| CWE | CWE-22: Path Traversal |
| GHSA | GHSA-rqfm-pw34-r7j6 |
공개 GHSA의 패키지 메타데이터와 연구 저장소에는 영향 범위가
< v8.9.6.4, 패치 버전이v8.9.7로 함께 표기되어 있습니다. 본문은 최종 CVE 레코드의 영향 범위인< v8.9.7을 기준으로 작성했습니다.
배경: Notepad++의 세션 복원
Notepad++는 종료 당시 열려 있던 탭과 저장되지 않은 변경 사항을 session.xml에 기록합니다. 스냅샷 모드가 활성화되어 있으면 각 <File> 항목의 backupFilePath를 이용해 백업 파일을 다시 불러옵니다.
정상적인 backupFilePath는 Notepad++ 사용자 경로 아래의 backup 디렉터리를 가리켜야 합니다.
기존 검증 방식
session.xml에서backupFilePath속성을 읽음getUserPath() + "\\backup\\"으로 허용된 백업 경로를 생성- 입력 경로가 허용 경로로 시작하는지
starts_with()로 검사 - 접두사가 일치하지 않으면 파일명만 추출해 백업 디렉터리 아래 경로로 재구성
- 검사를 통과한 경로를 세션 복원 정보에 저장하고 파일을 로드
문제는 3번 검사가 문자열에 적힌 경로만 보고, 운영체제가 실제로 해석할 정규화된 경로는 확인하지 않았다는 점입니다.
취약점 분석: starts_with() 접두사 검사 우회
경로 문자열에는 동일한 위치를 여러 형태로 표현할 수 있습니다. 예를 들어 다음 두 값은 문자열로는 다르지만 Windows가 해석한 최종 위치는 같습니다.
C:\lab\npp\backup\..\..\..\Windows\System32\drivers\etc\hostsC:\Windows\System32\drivers\etc\hosts취약한 코드는 첫 번째 문자열이 C:\lab\npp\backup\으로 시작한다는 사실만 확인합니다. 따라서 접두사 검사는 통과하지만, 파일을 여는 시점에는 ..\가 해석되어 백업 디렉터리 밖의 파일에 접근하게 됩니다.
| 단계 | 사용되는 경로 | 결과 |
|---|---|---|
| 검사 (CHECK) | 원본 backupFilePath 문자열 | starts_with(backupDir) → 통과 |
| 사용 (USE) | Windows가 정규화한 실제 경로 | ..\ 해석 후 백업 디렉터리 밖의 파일을 엶 |
검사한 경로 표현과 실제 사용한 경로 표현이 다릅니다.
취약한 코드 (Parameters.cpp: 3191–3218)
const wchar_t* pBackupFilePath = wmc.char2wchar( NppXml::attribute(childNode, "backupFilePath"), CP_UTF8);std::wstring currentBackupFilePath = NppParameters::getInstance().getUserPath() + L"\\backup\\";
if (pBackupFilePath){ std::wstring backupFilePath = pBackupFilePath; if (!backupFilePath.starts_with(currentBackupFilePath)) { // reconstruct backupFilePath wchar_t* fn = ::PathFindFileNameW(pBackupFilePath); currentBackupFilePath += fn; pBackupFilePath = currentBackupFilePath.c_str(); }}
sessionFileInfo sfi( wstrFileName.c_str(), wstrLangName.c_str(), encoding, isUserReadOnly, isPinned, isUntitleTabRenamed, position, pBackupFilePath, fileModifiedTimestamp, mapPosition);backupFilePath.starts_with(currentBackupFilePath)가 참이면 경로 재구성 로직은 실행되지 않습니다. 공격자가 넣은 원본 경로가 그대로 sessionFileInfo에 전달되고, 이후 파일 I/O 과정에서 Windows가 ..\를 해석합니다.
패치 커밋을 기준으로 실제 취약 코드와 수정 지점은 PowerEditor/src/Parameters.cpp입니다.
공격 시나리오
전제 조건
session.xml쓰기 권한- 스냅샷/백업 모드 활성화
- 대상 파일을 Notepad++ 프로세스 권한으로 읽을 수 있어야 함
- 세션 복원이 실행되는 Notepad++ 시작 과정
포터블 설치에서는 session.xml과 config.xml이 notepad++.exe와 같은 디렉터리에 있습니다. 스냅샷 모드가 이미 활성화되어 있지 않다면 config.xml을 수정할 권한도 필요합니다.
공격 흐름
session.xml의<File>항목에 조작된backupFilePath를 삽입- 경로 앞부분에 정상 백업 디렉터리를 배치하고 뒤에
..\시퀀스를 추가 - Notepad++ 실행 후 세션 복원 시작
- 원본 문자열이 정상 백업 경로로 시작하므로
starts_with()검사 통과 - Windows가
..\를 해석해 백업 디렉터리 밖의 대상 파일을 엶 - 대상 파일 내용이 Notepad++ 편집기 탭에 표시됨
예시 페이로드는 다음과 같은 형태입니다.
<backupDir>\..\..\..\Windows\System32\drivers\etc\hosts경로 해석 비교
| 구분 | 값 |
|---|---|
| 저장된 원본 | <backupDir>\..\..\..\Windows\System32\drivers\etc\hosts |
| 접두사 검사 | <backupDir>\로 시작 → 통과 |
| OS 정규화 결과 | C:\Windows\System32\drivers\etc\hosts |
| 최종 결과 | 백업 디렉터리 밖의 파일이 편집기 탭에 로드됨 |
영향
| 항목 | 내용 |
|---|---|
| 공격 유형 | 임의 파일 읽기 (Path Traversal) |
| 공격 위치 | Local |
| 필요 권한 | Low — session.xml 쓰기 권한 |
| 사용자 상호작용 | Passive — 다음 실행 및 세션 복원 필요 |
| 관리자 권한 / UAC | 불필요 / 없음 |
| 노출 범위 | 현재 사용자 권한으로 읽을 수 있는 파일 |
| 무결성 / 가용성 영향 | 없음 |
| CVSS v4.0 | 5.1 (AV:L/AC:L/AT:N/PR:L/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N) |
SSH 개인 키, .env, 애플리케이션 설정 파일, 자격 증명 관련 파일 등 Notepad++ 실행 사용자에게 읽기 권한이 있는 데이터가 편집기 탭에 노출될 수 있습니다.
PoC
재현 절차와 PowerShell PoC는 다음 자료에 공개되어 있습니다.
실행 환경
- Notepad++ v8.9.6.2 portable x64
- Windows 11 Pro x64
- 포터블 모드를 위한
doLocalConf.xml session.xml쓰기 권한- 스냅샷 모드와
backup디렉터리
검증 결과
PoC에서
C:\Windows\System32\drivers\etc\hosts가 경로 순회를 통해 Notepad++ 편집기 탭에 로드되는 것을 확인했습니다.
패치 분석
v8.9.7에 포함된 패치 커밋 7e66f36은 접두사를 검사하기 전에 PathCchCanonicalize()로 입력 경로를 정규화합니다.
const wchar_t* pBackupFilePath = wmc.char2wchar( NppXml::attribute(childNode, "backupFilePath"), CP_UTF8);wchar_t normalizedBackupFilePath[MAX_PATH]{};
if (pBackupFilePath){ ::PathCchCanonicalize( normalizedBackupFilePath, MAX_PATH, pBackupFilePath );}
std::wstring currentBackupFilePath = NppParameters::getInstance().getUserPath() + L"\\backup\\";
if (normalizedBackupFilePath[0]){ std::wstring backupFilePath = normalizedBackupFilePath; if (!backupFilePath.starts_with(currentBackupFilePath)) { wchar_t* fn = ::PathFindFileNameW(normalizedBackupFilePath); currentBackupFilePath += fn; StringCchCopyW( normalizedBackupFilePath, MAX_PATH, currentBackupFilePath.c_str() ); }}정규화 과정에서 ..\가 먼저 제거되므로, 백업 디렉터리 밖을 가리키는 실제 경로는 더 이상 허용된 접두사로 시작하지 않습니다. 검사를 통과하지 못한 경로는 파일명만 유지한 채 정상 백업 디렉터리 아래로 재구성됩니다.
| 항목 | 취약 버전 | v8.9.7 패치 |
|---|---|---|
| 검사 대상 | 원본 경로 문자열 | PathCchCanonicalize() 결과 |
..\ 처리 시점 | 파일 사용 시 OS가 처리 | 접두사 검사 전에 처리 |
| 세션에 전달되는 값 | 원본 pBackupFilePath | normalizedBackupFilePath |
| 디렉터리 이탈 경로 | 접두사가 맞으면 허용 | 정규화 후 감지해 재구성 |
검사와 사용이 동일한 정규화 경로를 기준으로 동작하면서 접두사 검사 우회가 차단됩니다.
공개 타임라인
| 날짜 | 내용 |
|---|---|
| 2026-06-05 | 연구 저장소에 기재된 취약점 공개일 |
| 2026-06-05 (UTC) | 패치 커밋 7e66f36 작성 |
| 2026-07-14 | Notepad++ v8.9.7 릴리즈 및 GHSA 공개 |
| 2026-08-17 | CVE-2026-52886 레코드 공개 |
마무리
이번 취약점은 경로의 문자열 형태가 아니라 정규화된 최종 위치를 검증해야 한다는 원칙을 보여줍니다. starts_with() 같은 접두사 비교는 입력 경로에 ..\, 중복 구분자, 상대 경로 표현이 남아 있다면 보안 경계로 사용할 수 없습니다.
경로 검증과 실제 파일 접근이 같은 정규화 결과를 사용하도록 수정하면서 취약점이 해결됐습니다. 개발팀의 대응에 감사드립니다.