경로 조작 (Path Traversal)

Path Traversal

설명

경로 조작(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 예시는 요청 본문을 파싱하는 미들웨어가 구성되어 있다고 가정합니다.

참조