서버사이드 요청 위조 (SSRF)

Server-Side Request Forgery (SSRF)

설명

SSRF는 서버가 외부로 나가는 HTTP 요청의 대상(URL/host)을 사용자 입력으로부터 직접 받아 구성할 때 발생합니다. 공격자는 이 입력을 조작해 서버가 내부망(127.0.0.1, 10.x.x.x 등)이나 보호된 서비스(예: 클라우드 메타데이터 엔드포인트)로 요청을 보내게 만들 수 있습니다. 이를 통해 내부 자원 탐색, 민감 정보 획득, 권한 우회 등의 공격이 가능합니다. 또한 리다이렉트, DNS rebinding, open redirect 등을 이용해 검증을 우회하기도 합니다.

잠재적 영향

  • 내부망 우회 접근: 서버가 대신 내부 IP/호스트로 접속하여 방화벽 안쪽 자원에 접근할 수 있습니다.
  • 민감 정보 유출: DB 관리 콘솔, 내부 API, 클라우드 메타데이터(예: 169.254.169.254) 등에서 토큰/크리덴셜이 노출될 수 있습니다.
  • 인증 우회 및 권한 상승: 내부 전용 관리 엔드포인트를 호출하여 인증을 우회하거나 더 높은 권한을 얻을 수 있습니다.
  • 서비스 장애(DoS): 대량의 외부/내부 요청을 유발해 자원을 고갈시키거나 백엔드 서비스에 과부하를 줄 수 있습니다.
  • 공격 인프라로 악용: 내부망 스캐닝, 포트 스캔의 중계 지점으로 서버가 악용될 수 있습니다.

해결 방법

  • Host allow-list: URL의 host는 고정 값 또는 사전 정의된 allow-list에서만 선택하세요. 가능한 경우 식별자(ID) → 고정 URL 매핑을 사용하세요.
  • Path 검증/정규화: 경로는 허용 목록 기반으로 제한하고, '..' 등 디렉터리 이동을 금지하며, URL을 표준 파서(new URL 등)로 정규화 후 사용하세요.
  • 내부 자원 차단: 내부·특수 주소 대역(127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16 등), localhost, 메타데이터 엔드포인트로의 접근을 차단하세요.
  • 프로토콜 제한: http/https만 허용하고 file://, ftp://, gopher:// 등은 금지하세요.
  • 리다이렉트 제어: 자동 리다이렉트를 비활성화하거나, 리다이렉트 목적지도 동일한 검증(allow-list, 내부 IP 차단)을 재적용하세요.

예시

변경 전

javascript
// 변경 전: 사용자 입력 URL을 그대로 요청 -> SSRF 가능
const express = require("express");
const axios = require("axios");
const app = express();

app.get("/proxy", async (req, res) => {
  try {
    const url = req.query.url; // 예: http://127.0.0.1:8080/admin
    if (!url) return res.status(400).send("url required");

    // 취약: 사용자가 준 URL을 그대로 사용
    const resp = await axios.get(url, { timeout: 5000 });
    res.send(resp.data);
  } catch (e) {
    res.status(502).send("bad gateway");
  }
});

app.listen(3000);

변경 후

javascript
// 변경 후: 식별자 → 고정 URL 매핑 + 안전한 path 제한
const express = require("express");
const axios = require("axios");
const app = express();

// 1) 허용된 서비스 식별자 → 고정 base URL 매핑
const SERVICE_BASE = new Map([
  ["status", new URL("https://status.example.com")],
  ["api", new URL("https://api.example.com")],
]);

// 2) path 허용 목록: 하나의 '/' 로 시작하고 영숫자, /, -, _만 허용
//    '//evil.example' 같은 protocol-relative URL과 '..' 경로는 거부한다.
const SAFE_PATH = /^\/(?!\/)[A-Za-z0-9_\/-]*$/;

app.get("/proxy-safe", async (req, res) => {
  try {
    const { service, path = "/" } = req.query;

    // 서비스 아이디 검증(allow-list)
    if (typeof service !== "string" || typeof path !== "string") {
      return res.status(400).send("invalid parameters");
    }
    const base = SERVICE_BASE.get(service);
    if (!base) return res.status(400).send("invalid service");

    // 경로 검증 및 정규화
    if (!SAFE_PATH.test(path) || path.includes("..")) {
      return res.status(400).send("invalid path");
    }

    // URL 표준 파서 사용으로 조합 오류 방지
    const target = new URL(path, base);
    if (target.origin !== base.origin) {
      return res.status(400).send("invalid target");
    }

    // 리다이렉트 제한 및 타임아웃 설정
    const resp = await axios.get(target.toString(), {
      timeout: 4000,
      maxRedirects: 0, // 리다이렉트로 내부망 우회 방지
      responseType: "arraybuffer",
      maxContentLength: 1024 * 1024,
      validateStatus: (s) => s < 400,
    });

    // 원격 콘텐츠를 현재 애플리케이션 origin의 HTML로 실행하지 않음
    res.status(resp.status);
    res.set("Content-Type", "application/octet-stream");
    res.set("X-Content-Type-Options", "nosniff");
    res.send(Buffer.from(resp.data));
  } catch (e) {
    // 리다이렉트/검증 실패 등은 상세 정보 노출 없이 에러 처리
    res.status(502).send("bad gateway");
  }
});

app.listen(3000);

설명:

  • 변경 전: 사용자 입력 URL을 그대로 http/https 요청에 사용하여, 공격자가 내부 IP(127.0.0.1, 169.254.169.254 등)나 사설 서비스로 서버를 대신 접근시키는 SSRF가 가능합니다. 또한 리다이렉트를 허용하면 외부 → 내부로 우회될 수 있습니다.
  • 변경 후: host는 서버가 소유한 Map의 고정 URL에서만 선택하고, path는 하나의 /로 시작하도록 제한해 //evil.example 같은 protocol-relative URL과 .. 경로를 차단합니다.
  • URL 표준 파서로 조합한 뒤 결과 origin을 고정 base origin과 다시 비교하고, maxRedirects: 0으로 리다이렉트 우회를 방지합니다.
  • 원격 응답은 1 MiB로 제한해 바이너리로 받고 application/octet-stream과 nosniff를 적용하여 현재 애플리케이션 origin의 실행 가능한 HTML로 재해석되지 않게 합니다.
  • 이 방식은 사용자가 임의의 host/IP를 주입할 수 없게 하여 SSRF 공격 면을 줄입니다. 허용 호스트의 DNS와 실제 연결 대상, 네트워크 송신 정책도 신뢰할 수 있게 관리하세요.

참조