설명
경로 조작(Path Traversal)은 공격자가 파일 경로에 '../' 등과 같은 특수 문자를 포함시켜 애플리케이션의 허가되지 않은 파일 또는 디렉터리에 접근하는 취약점입니다. 주로 사용자로부터 받은 입력값을 검증 없이 파일 시스템 함수에 넘길 때 발생합니다. 공격자는 이를 통해 중요한 시스템 파일이나 민감 정보를 읽거나 덮어쓸 수 있습니다.
잠재적 영향
- 파일 무단 접근 (허가되지 않은 파일 접근): 공격자가 중요한 시스템 파일이나 다른 사용자의 데이터에 접근할 수 있습니다.
- 데이터 변조 및 삭제 (데이터 훼손): 공격자가 중요한 파일을 변경하거나 삭제할 수 있습니다.
- 서비스 장애 유발 (서비스 거부 공격): 시스템의 핵심 파일이 손상되어 서비스가 정상적으로 동작하지 않을 수 있습니다.
해결 방법
- 사용자 입력값에서 '../', '/', '\' 등 경로 변조 문자열이 포함되어 있는지 검사하세요.
- 특정 디렉터리 내에서만 파일 접근을 허용하고, 절대 경로 사용을 제한하세요.
- Node.js의 path.normalize() 등을 이용하여 경로를 정규화 후, 기준 디렉터리 범위 내에 있는지 검증하세요.
- 파일명 허용 목록이나 미리 정의된 규칙만 허용하세요.
fs.writeFile(path, data)처럼 첫 번째 인자가 경로인 API와fs.write(fd, data)처럼 첫 번째 인자가 파일 디스크립터인 API를 구분하세요. 사용자 입력이 파일 내용이나 파일 디스크립터 뒤의 데이터 인자에만 들어가는 경우는 경로 조작과 별도로 판단합니다.
예시
변경 전
javascript
const fs = require('fs');
const express = require('express');
const app = express();
app.get('/download', (req, res) => {
// 사용자의 입력값을 그대로 파일 시스템 함수에 사용 (취약)
const fileName = req.query.file;
fs.readFile(fileName, (err, data) => {
if (err) return res.status(404).send('File not found');
res.send(data);
});
});
변경 후
javascript
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();
// 서버만 쓸 수 있는 디렉터리여야 검사와 읽기 사이의 링크 교체를 막을 수 있음
const BASE_DIR = fs.realpathSync(path.join(__dirname, 'files'));
function isInside(baseDir, targetPath) {
const relative = path.relative(baseDir, targetPath);
return (
relative !== '' &&
relative !== '..' &&
!relative.startsWith('..' + path.sep) &&
!path.isAbsolute(relative)
);
}
app.get('/download', (req, res) => {
const rawName = req.query.file;
if (typeof rawName !== 'string') {
return res.status(400).send('Invalid file path');
}
// 파일명만 허용하므로 경로 구성요소가 있으면 정제하지 않고 거부
const fileName = path.basename(rawName);
if (fileName !== rawName || !/^[a-zA-Z0-9._-]+$/.test(fileName)) {
return res.status(400).send('Invalid file path');
}
const candidate = path.resolve(BASE_DIR, fileName);
if (!isInside(BASE_DIR, candidate)) {
return res.status(400).send('Invalid file path');
}
let requestedPath;
try {
requestedPath = fs.realpathSync(candidate);
} catch {
return res.status(404).send('File not found');
}
if (!isInside(BASE_DIR, requestedPath)) {
return res.status(400).send('Invalid file path');
}
fs.readFile(requestedPath, (err, data) => {
if (err) return res.status(404).send('File not found');
res.send(data);
});
});
app.post('/message', (req, res) => {
// OK: 경로는 서버가 고정하고, 사용자 입력은 파일 내용으로만 사용
fs.writeFile(path.join(BASE_DIR, 'message.txt'), req.body.message, (err) => {
if (err) return res.status(500).send('write failed');
res.send('saved');
});
});
설명:
- 변경 전: 사용자의 입력값(fileName)을 검증하지 않고 그대로 파일 시스템 함수(fs.readFile)에 사용해서 '../' 등 경로 조작이 가능해집니다. 공격자는 프로세스에 읽기 권한이 있는 파일에 접근할 수 있습니다.
- 변경 후: 문자열 타입과
path.basename()의 동일성을 확인해 디렉터리 구성요소를 거부하고, 정규화된 경로와 심볼릭 링크를 해석한 실제 경로가 모두 서버 관리 BASE_DIR 내부인지 검증합니다. 또한 사용자 입력이 파일 내용 인자에만 들어가고 경로 인자는 서버가 고정한 값이면 경로 조작 위험과 구분됩니다.
POST 예시는 요청 본문을 파싱하는 미들웨어가 구성되어 있다고 가정합니다.