설명
브라우저 윈도우·iframe 간 통신인 postMessage에서 수신 측이 message 이벤트의 event.origin(보낸 창의 스킴/호스트/포트)을 엄격히 검증하지 않으면 발생합니다. 공격자는 악성 사이트나 임의의 도메인에서 메시지를 보내 수신 측이 이를 신뢰하도록 유도할 수 있습니다. 공격자가 대상 창의 참조를 얻어 메시지를 전달할 수 있어야 합니다. 특히 핸들러가 메시지 내용을 그대로 실행(eval)하거나 권한 있는 동작을 수행하면, 공격 입력으로 기능 오용, 데이터 노출, 코드 실행 등이 가능합니다.
잠재적 영향
- 교차 출처 데이터 유출: 검증 없이 신뢰해 민감 정보(토큰, 사용자 데이터)를 외부로 전송하거나 콘솔/DOM에 노출할 수 있음
- 무단 동작/코드 실행: 핸들러가 eval/Function 등을 사용하거나 중요한 함수를 호출하면, 공격자가 임의 동작(예: 설정 변경, 결제 수행)을 트리거할 수 있음
- 권한 오용 및 상태 변경: postMessage를 내부 API처럼 사용 시, 인증된 컨텍스트에서 공격자의 명령이 수행되어 권한 상승 효과가 날 수 있음
- 서비스 거부(DoS): 대량 메시지로 이벤트 큐를 과부하시켜 UI 멈춤, 리소스 고갈 유발
해결 방법
- 엄격한 Origin 허용 목록: event.origin을 스킴/호스트/포트까지 정확히 일치 비교(===)로 검증하세요. 부분 일치(startsWith, includes)만으로 허용하지 마세요.
- sender 식별: 가능한 경우 event.source가 예상한 window(예: 특정 iframe.contentWindow)인지도 함께 확인하세요.
- 데이터 검증: event.data를 예상된 스키마(JSON 형태, type 필드 등)로 파싱·검증하고, 위험한 API(eval, new Function) 사용하지 마세요.
- 실패 시 즉시 차단: 검증 실패 시 return하여 아무 동작도 하지 마세요. 로그만 남기는 것은 불충분합니다.
- 전송 측 보강: postMessage 보낼 때 민감한 메시지의 targetOrigin에 '*' 대신 정확한 Origin을 지정하세요.
예시
변경 전
javascript
// 변경 전: origin 검증 없음, 위험 API/동작 수행
window.addEventListener("message", (e) => {
// 외부 어떤 창이든 보낸 메시지를 그대로 신뢰
if (e.data && e.data.type === "setTheme") {
document.documentElement.setAttribute("data-theme", e.data.value);
}
// 매우 위험: 수신 데이터 실행
if (e.data && e.data.exec) {
eval(e.data.exec); // 공격자가 임의 코드 실행 가능
}
// 토큰 등 민감 정보 설정/전송
if (e.data && e.data.accessToken) {
localStorage.setItem("access_token", e.data.accessToken);
}
});
변경 후
javascript
// 변경 후: 엄격한 origin & source 검증 + 데이터 스키마 검증
(() => {
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
// 예상된 보낸 쪽 창(예: 특정 iframe)을 알고 있다면 참조 보관
const settingsFrame = document.getElementById("settings-frame");
const expectedSource = settingsFrame ? settingsFrame.contentWindow : null;
function isValidPayload(payload) {
// 최소한의 스키마 검증: 객체이며 허용된 타입만 허용
if (!payload || typeof payload !== "object") return false;
const allowedTypes = new Set(["setTheme", "ping"]);
return typeof payload.type === "string" && allowedTypes.has(payload.type);
}
window.addEventListener("message", (event) => {
// 1) origin 정확 일치 검사
if (!ALLOWED_ORIGINS.has(event.origin)) return;
// 2) sender(window) 식별 검사(가능한 경우)
if (expectedSource && event.source !== expectedSource) return;
// 3) 데이터 검증 및 안전 처리
const data =
typeof event.data === "string"
? (() => {
try {
return JSON.parse(event.data);
} catch {
return null;
}
})()
: event.data;
if (!isValidPayload(data)) return;
switch (data.type) {
case "setTheme":
if (
typeof data.value === "string" &&
/^(light|dark)$/.test(data.value)
) {
document.documentElement.setAttribute("data-theme", data.value);
}
break;
case "ping":
// 필요 시 안전한 응답 전송: 정확한 targetOrigin 지정 필수
event.source &&
event.source.postMessage(
JSON.stringify({ type: "pong" }),
event.origin
);
break;
default:
// no-op
break;
}
});
})();
설명:
- 변경 전: 수신 핸들러가 event.origin을 확인하지 않아 임의의(악성) 도메인이 보낸 메시지를 신뢰합니다. 또한 eval로 수신 데이터를 실행하고, 토큰/설정 변경 같은 민감 동작을 수행하여 임의 코드 실행, 권한 오용, 데이터 노출로 이어질 수 있습니다.
- 변경 후: 허용 목록과 origin을 정확히 비교하고, 예상한 iframe 참조가 있을 때 event.source도 확인합니다. 특정 창만 허용해야 한다면 그 참조가 없는 상태로 처리하지 마세요. 수신 데이터는 JSON 파싱과 스키마 검증으로 형태·값을 확인하고, 위험 API(eval)를 사용하지 않습니다. 실패 시 즉시 return하여 아무 동작도 하지 않아 공격 범위를 최소화합니다.