이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Same-Origin Policy는 무엇을 막고 무엇을 막지 못하는가?
- 같은 origin과 같은 site는 왜 다른 개념인가?
- 브라우저 보안 모델을 서버 권한 검사와 어떻게 함께 설계해야 하는가?
개요
Same-Origin Policy, SOP는 브라우저의 기본 보안 경계다.
Origin은 scheme, host, port의 조합이다.
https://app.example.com:443이 origin은 다음과 다르다.
https://api.example.com:443
http://app.example.com:80
https://app.example.com:8443SOP는 한 origin의 script가 다른 origin의 응답을 마음대로 읽지 못하게 한다. 그러나 요청 전송 자체를 모두 막는 것은 아니다.
공격 시나리오
피해자는 https://bank.example.com에 로그인해 있고 session cookie를 가지고 있다.
공격자는 https://evil.example에 다음 HTML을 둔다.
<form action="https://bank.example.com/api/account/email" method="POST">
<input name="email" value="attacker@example.com">
</form>
<script>document.forms[0].submit()</script>SOP 때문에 공격자의 JavaScript가 bank 응답을 읽기는 어렵다. 하지만 피해자 브라우저가 요청을 보내고 cookie를 자동 전송할 수는 있다.
상태 변경 API가 CSRF token 없이 동작하면 응답을 읽지 못해도 공격은 성공한다.
취약한 요청/응답 예시
POST /api/account/email HTTP/1.1
Host: bank.example.com
Cookie: SESSION=victim-session
Origin: https://evil.example
Content-Type: application/x-www-form-urlencoded
email=attacker@example.comHTTP/1.1 200 OK
Content-Type: text/html
email changed브라우저 보안 모델이 응답 읽기를 제한해도 서버가 상태 변경을 허용하면 피해는 이미 발생한다.
주제별 핵심 판단
SOP가 주로 막는 것은 cross-origin read다.
공격 사이트의 JavaScript가 https://bank.example.com/account 응답 본문을 읽는 것은 기본적으로 막힌다.
그러나 다음은 별도 정책을 봐야 한다.
- form submit으로 요청 보내기
- image, script, link 같은 subresource 로딩
- top-level navigation
- 쿠키 자동 전송
- CORS로 명시적으로 열린 응답 읽기
따라서 SOP를 “다른 사이트가 우리 서버에 요청하지 못하게 하는 기능”으로 이해하면 CSRF를 놓친다.
개선된 요청/응답 예시
SOP를 이해할 때 서버는 다음을 분리한다.
- 개선된 응답은 브라우저가 다른 origin 스크립트에 민감 응답을 노출하지 않도록 CORS와 cookie 속성을 함께 맞춘다.
- 서버는 SOP를 믿고 권한 검사를 생략하지 않으며 API마다 인증과 인가를 수행한다.
방어 설계
read protection:
- SOP
- CORS allowlist
- CORP/CORB 같은 응답 격리 정책
state change protection:
- CSRF token
- SameSite cookie
- Origin/Referer 검증
- 재인증 또는 step-up
authorization:
- principal과 resource/action 관계 검증SOP는 authorization을 대신하지 않는다. 공격자가 브라우저를 우회해 서버 간 요청을 보내면 SOP는 적용되지 않는다.
Spring/HTTP 예시
쿠키 기반 웹 애플리케이션에서는 CSRF를 유지한다.
@Bean
SecurityFilterChain browserApp(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.build();
}Origin 검증은 민감 상태 변경에서 보조 방어로 쓸 수 있다.
public void verifyOrigin(String origin) {
if (!Set.of("https://app.example.com").contains(origin)) {
audit.browserPolicyRejected("origin_mismatch", origin);
throw new AccessDeniedException("invalid origin");
}
}Origin header는 없는 경우도 있으므로 정책상 어떻게 처리할지 정해야 한다.
운영 로그와 감사 지점
level=WARN event=browser_policy_rejected result=blocked policy=sop_boundary action=change_email origin=https://evil.example expected_origin=https://app.example.com reason=origin_mismatch trace_id=312fac90level=INFO event=cross_origin_request_observed result=allowed origin=https://app.example.com path=/api/account/email action=change_email csrf_valid=true trace_id=312fac91로그에는 Origin, path, action, CSRF 검증 결과, decision reason을 남긴다. session cookie 원문은 남기지 않는다.
실전 판단 기준
- 다른 origin의 form submit으로 상태 변경이 성공하지 않는가?
- SOP는 읽기 노출을 막는 브라우저 정책이지 요청 전송 자체를 항상 막는 장치가 아니다.
- 서버 로그에는 Origin, method, credential 포함 여부를 확인할 수 있어야 한다.
테스트 포인트
- 정상 origin과 비정상 origin을 구분해 로그가 남는가?
- CORS를 막아도 서버 authorization 테스트는 별도로 존재하는가?
- 같은 site와 같은 origin을 혼동해 cookie나 CORS 정책을 넓게 열지 않았는가?
- GET 요청에 상태 변경이 숨어 있지 않은가?
- 민감 action에는 CSRF token 또는 step-up이 있는가?
위험 신호!
- SOP가 있으니 CSRF 방어가 필요 없다고 말한다.
- CORS 에러가 안 나면 보안상 안전하다고 판단한다.
Origin을 보조 신호로 쓰면서 서버 권한 검사를 생략한다.- GET 요청으로 상태를 변경한다.
- 같은 site와 같은 origin을 같은 말로 쓴다.
- 서버 간 호출도 CORS가 막아 줄 것이라고 기대한다.
확인 질문
확인 질문
- SOP가 주로 제한하는 것은 무엇인가?
- 한 origin의 script가 다른 origin의 응답을 읽는 것을 제한한다.
- SOP가 CSRF를 완전히 막지 못하는 이유는 무엇인가?
- form submit 같은 요청 전송과 cookie 자동 전송은 여전히 가능할 수 있기 때문이다.
- 서버 권한 검사가 여전히 필요한 이유는 무엇인가?
- SOP는 브라우저 정책이며 서버 간 요청, 직접 API 호출, 같은 origin 내 악성 script를 막는 authorization 정책이 아니기 때문이다.