설명
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와 실제 연결 대상, 네트워크 송신 정책도 신뢰할 수 있게 관리하세요.