설명
경로 조작은 사용자 입력을 그대로 파일 경로에 사용할 때 발생합니다. 공격자는 ../, 절대 경로(/, C:\ 등), 또는 심볼릭 링크를 이용해 애플리케이션이 의도한 디렉터리 밖의 파일에 접근하도록 유도할 수 있습니다. 이를 통해 /etc/passwd 같은 시스템 파일을 읽거나, 애플리케이션 설정/로그를 덮어쓰고 삭제할 수 있습니다.
잠재적 영향
- Sensitive Data Exposure (민감 정보 노출): 디렉터리 밖의 시스템 파일, 소스 코드, 환경설정(.env 등) 유출
- Data Tampering/Deletion (데이터 변경/삭제): 업로드/로그/설정 파일을 덮어쓰기 또는 삭제하여 기능 오작동 유발
- Privilege Escalation (권한 상승): 서비스 권한으로 임의 파일 쓰기를 악용해 실행 경로에 스크립트 배치 등 간접적 권한 상승 가능
- Service Disruption (서비스 중단): 핵심 파일 삭제/손상으로 애플리케이션 장애 또는 DoS 유발
해결 방법
- 사용자 입력으로 경로를 만들지 말고, 반드시 정규화와 경계 확인을 수행하세요.
- const resolved = fs.realpathSync(path.resolve(SAFE_ROOT, input));
- resolved가 SAFE_ROOT + path.sep로 시작하는지 확인하고, 아니라면 거부하세요.
- 단순 파일명만 허용해야 한다면 allow-list(확장자/패턴) 또는 sanitize-filename 사용, 혹은 엄격한 정규식(예: ^[a-zA-Z0-9._-]{1,100}$) 검증을 적용하세요.
- 절대 경로, .., 빈 값 등은 즉시 차단하세요.
realpath로 기존 경로의 링크를 해소하세요.realpath나fs.lstat로 검사한 뒤에도 파일이 바뀔 수 있으므로 기준 디렉터리와 하위 경로를 공격자가 수정할 수 없도록 보호하세요.- 파일 접근 권한을 최소화하고(Least Privilege), 에러 메시지에는 실제 경로를 노출하지 마세요.
- 업로드/다운로드 루트를 고정(SAFE_ROOT)하고, 그 밖의 경로 접근은 금지하세요.
예시
변경 전
javascript
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// 취약한 예: 사용자 입력을 join만 하고 사용 (.. 탈출 가능)
app.get('/view', (req, res) => {
const name = req.query.f; // 예: ../../etc/passwd
const unsafePath = path.join(__dirname, 'public', name);
fs.createReadStream(unsafePath)
.on('error', () => res.status(404).end('not found'))
.pipe(res);
});
app.listen(3000);
변경 후
javascript
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// SAFE_ROOT는 공격자가 수정할 수 없는 서버 관리 디렉터리여야 함
const SAFE_ROOT = fs.realpathSync(path.resolve(__dirname, 'public'));
function isInside(baseDir, targetPath) {
const relative = path.relative(baseDir, targetPath);
return (
relative !== '' &&
relative !== '..' &&
!relative.startsWith('..' + path.sep) &&
!path.isAbsolute(relative)
);
}
app.get('/view', (req, res) => {
const name = req.query.f;
if (typeof name !== 'string') {
return res.status(400).send('parameter f is required');
}
// 경로 정규화 + 심볼릭 링크 해소
const candidate = path.resolve(SAFE_ROOT, name);
let resolved;
try {
resolved = fs.realpathSync(candidate);
} catch {
return res.status(404).end('not found');
}
// 루트 경계 확인 (SAFE_ROOT 하위 파일만 허용)
if (!isInside(SAFE_ROOT, resolved)) {
return res.status(403).end('forbidden');
}
// 검증된 경로만 사용
const stream = fs.createReadStream(resolved);
stream.on('error', () => res.status(404).end('not found'));
stream.pipe(res);
});
app.listen(3000);
설명:
- 변경 전: 사용자 입력
f를 검증 없이 경로 결합에 사용합니다.path.join은..구성 요소를 정규화하지만 결과가 기준 디렉터리 안에 남는지는 보장하지 않으므로, 정규화된 경로가 상위 디렉터리로 탈출해 애플리케이션 루트 밖의 파일에 접근할 수 있습니다. - 변경 후: 문자열 입력만 받은 뒤 SAFE_ROOT를 기준으로
path.resolve하고,fs.realpathSync로 심볼릭 링크를 해소합니다.path.relative로 실제 경로가 공격자가 수정할 수 없는 SAFE_ROOT의 하위 파일인지 확인한 뒤에만 접근합니다. 검사와 파일 열기 사이에도 해당 디렉터리 구조를 신뢰할 수 있어야 하므로, 공격자가 하위 경로를 변경할 수 있는 환경에서는 이 검사만으로 경쟁 조건까지 해결되지는 않습니다.