설명
클라이언트 사이드 요청 위조는 브라우저에서 사용자 입력을 그대로 사용해 fetch/XHR/axios 등의 요청 URL을 동적으로 만들 때 발생합니다. 공격자는 쿼리스트링, hash, 폼 입력 등에 조작 값을 주입해 의도하지 않은 경로(예: ../를 이용한 경로 탈출)나 허용되지 않은 호스트로 요청을 보낼 수 있습니다. 대상 API가 요청을 받아 상태를 변경한다면 응답을 읽지 못해도 영향이 생길 수 있고, 응답을 innerHTML로 렌더링하면 XSS로 이어질 수 있습니다. 실제 요청 가능 여부와 영향은 브라우저의 보안 정책, 자격 증명 옵션, 대상 API의 인증·인가에 따라 달라집니다.
잠재적 영향
- 내부/민감 엔드포인트 호출: 공격자가 경로 조작(../)이나 임의 호스트를 이용해 내부 API·관리 엔드포인트로 요청을 유도할 수 있습니다.
- 기능 오용 및 권한 우회: 인증된 사용자의 브라우저를 통해 의도치 않은 API 호출이 이뤄져 데이터 조회·변경 등 기능이 오남용될 수 있습니다.
- XSS/콘텐츠 변조: 응답을 innerHTML 등으로 신뢰 없이 렌더링할 경우 스크립트 실행(XSS)로 이어질 수 있습니다.
- 사용자 네트워크 남용: 사용자의 로컬/사내 네트워크 자원(예: 127.0.0.1, 사내 호스트)으로 원치 않는 요청을 보내는 데 악용될 수 있습니다.
해결 방법
- 호스트 고정 또는 허용 목록: URL의 호스트는 고정하거나 미리 정의한 허용 목록에서만 선택하게 하세요.
- 경로 유효성 검사: 입력 전체의 형식과 허용 범위를 검증하세요.
Number나parseInt로 변환하는 것만으로 검증이 끝나지는 않습니다.../,//,%2e%2e같은 경로 조작이 가능하지 않게 하세요. - URL 조립: 기준 URL을 고정하고 검증한 경로 요소만 조립하세요.
URL이나encodeURIComponent만으로 허용 경로가 보장되지는 않으므로, 최종 출처와 경로도 확인하세요. - 안전한 렌더링: 서버 응답을 DOM에 넣을 때 innerHTML 대신 textContent(또는 검증된 sanitizer)를 사용하세요.
- 필요 최소 권한 옵션: fetch 옵션은 credentials, method 등을 최소화하고, 서버 측 인증·인가와 CSRF 방어를 함께 적용하세요. CORS만으로 요청 전송이나 상태 변경을 막을 수는 없습니다.
예시
변경 전
javascript
async function showProfile() {
const qs = new URLSearchParams(location.search);
const next = qs.get("next"); // 예: https://evil.example/steal
const userId = qs.get("id"); // 예: ../../admin/metrics
// BAD 1: 신뢰할 수 없는 전체 URL 사용
const r1 = await fetch(next);
// BAD 2: 경로에 사용자 입력을 그대로 연결(경로 탈출 가능)
const r2 = await fetch("/api/user/" + userId);
const data = await r2.json();
// BAD 3: 응답을 검증 없이 innerHTML에 주입 → XSS 위험
document.getElementById("content").innerHTML = data.html;
}
변경 후
javascript
async function showProfileSafe() {
// 1) 허용된 호스트만 사용
const BASE = "https://api.example.com";
// 2) 경로 파라미터는 스키마 강제(숫자) 또는 엄격 검증
const qs = new URLSearchParams(location.search);
const rawId = qs.get("id");
const id = Number(rawId);
if (!Number.isInteger(id) || id < 0) {
throw new Error("invalid id");
}
// 3) URL은 base를 고정하고, segment만 안전하게 조립
const url = new URL(`/v1/user/${id}`, BASE);
const res = await fetch(url.toString(), {
method: "GET",
credentials: "same-origin",
});
const data = await res.json();
// 4) 콘텐츠 출력 시 innerHTML 대신 textContent 사용
const box = document.getElementById("content");
box.textContent = data.name; // HTML이 필요하면 검증된 sanitizer 사용
}
설명:
- 변경 전:
- 사용자 입력(next, id)을 그대로 fetch의 URL로 사용하여, 임의 호스트로의 요청과 경로 탈출이 가능합니다.
- 서버 응답을 innerHTML에 직접 주입해 XSS로 이어질 수 있습니다.
- 변경 후:
- 호스트를 고정하고 0 이상의 정수만 경로에 넣어
../같은 조작을 차단합니다. 실제로 허용할 ID 범위와 접근 권한은 서버에서도 검증해야 합니다. - URL(base)로 안전하게 조립해 전체 URL이 사용자 입력으로 대체되지 않게 합니다.
- DOM 렌더링 시 textContent를 사용하여 스크립트 실행을 방지합니다.
- 호스트를 고정하고 0 이상의 정수만 경로에 넣어