🐕 SOP (Same - origin - policy) 동일 출처 정책
동일 출처 정책은 웹 애플리케이션의 중요한 보안 모델입니다.
동일 출처 정책은 같은 출처(Origin)의 리소스만 공유가 가능하다는 정책인데
인간이 보기엔 그냥 딱봐도 같은 출처인것도
여기서 많이 걸러져서 CORS를 모르고 있으면 CORS 에러를 만나게되는거네요
동일출처 정책은 위험할 수 있는 문서를 분리함으로써
공격받을 수 있는 경로를 줄여주는 역할을 수행합니다.
만일 이러한 제약이 없다면 CSRF (Cross-Site Request Forgery) 나 XSS(Cross-Site Scripting)등의
공격에 매우 취약해질 가능성이 생깁니다.
따라서 동일하지 않는 다른 출처의 스크립트가 실행되지 않도록
브라우저에서 사전에 방지해주는 것이 SOP 인거네요!
근데 그럼 컴퓨터가 중요하게 보는 출처는 어떤 것을 말할까요?
👻출처(origin)의 결정 규칙
https://ko.wikipedia.org/wiki/%EB%8F%99%EC%9D%BC-%EC%B6%9C%EC%B2%98_%EC%A0%95%EC%B1%85
위 사이트에서 예제를 가져왔습니다.
대충 뭐는 되고 뭐는 안되는지에 대한 이야기를 담고 있는데
가장 중요한 규칙은 세가지가 동일해야한다는 것입니다.
프로토콜 |
호스트 |
포트번호 |
요 세가지 중 하나라도 다르면 다른 출처로 인식한다는 뜻
그러니까 http냐 https냐와 같이 영문자 하나만으로도 다른출처가 될 수 있고
같은 호스트에 같은 프로토콜로 접근해도 포트번호가 다르면 다른 출처...
개같다 그죠
특히 포트번호는 생략하거나 다르게 바꿔쓰는 경우도 많아서 굉장히 헷갈리더라구요
이 세가지가 동일해야지만 동일 출처다!
🥶 출처 결정할 때 정말 헷갈리는 문제
일단 1번은 프로토콜이 다르고...
3번은 호스트가 다르니까...
2,4번??
3번은 브라우저가 스트링밸류를 써서 비교를하는데
로컬호스트와 127.0.0.1이 다른것으로 인식한다..
(이건 추가로 공부해야할듯)
2번이 맞는 이유는 http의 기본 포트가 80이라 생략이 가능하기때문에..
4번이 맞는 이유는 api/cors는 path니까.. 달라도 우리가 출처를 비교하는 공식은 지켰음
🌞CORS ( Cross Origin Resource Sharing) 교차 출처 리소스 공유
CORS는 추가 HTTP 헤더를 사용하여서 한 출처에서 실행중인 웹 애플리케이션이
다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록
브라우저에 알려주는 규약...에 가깝습니다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
자세한 정보는 Mozilla에서 확인해볼 수 있지만..
읽기가 싫은건 견디세요
1. HTTP 헤더를 기반으로 한다는게 일단 흥미롭고
2. 서버가 실제 요청을 허용하는 확인하기 위해 "실행 전" 요청을 하는 메커니즘이 있는 것
요정도가 일단은 흥미롭습니다.
CORS에게는 접근제어 시나리오라는 게 있는데 우선은 이걸 살펴봐요!
😋CORS의 접근제어 시나리오
1. 프리플라이트 요청 (Preflight Request) |
2. 단순 요청(Simple Request) |
3. 인증정보 포함 요청 (Credentialed Request) |
위와 같이 세가지의 시나리오가 있습니다.
https://developer.mozilla.org/ko/docs/Glossary/Preflight_request
위 mozilla 페이지에서 프리플라이트에 대한 정보를 찾아볼 수 있어요
😋프리플라이트 요청
프리플라이트의 메커니즘은 다음과 같아요
1. OPTIONS 메서드를 통해 다른 도메인의 리소스에 요청이 가능한지 확인 작업을 진행한다.
2. 1의 과정에서 만약 요청이 가능한 것을 확인했다면 실제 요청(Actual Request)를 보낸다.
대충 이런식으로 통신이 이루어지겠군요
프리플라이트를 먼저 보내보고 응답이 오면
실제 요청을 보내고 그에 대한 응답을 보내주는 식으로??
그런데 사전 요청에는 꼭 들어가야할 요청들이 있습니다.
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
다음 HTTP Header 코드는 클라이언트와 서버간의 프리플라이트 요청과 응답을 나타냅니다.
아까 프리플라이트에서 필요하다고 했던 OPTIONS 도 이 헤더에 있는걸 확인할 수 있네요
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
이 중 요 세가지는 프리플라이트 요청에서 물어봐줘야합니다.
Origin | 요청의 출처 |
Access-Control-Request-Method | 실제로 요청하고자 하는 메서드 |
Access-Control-Request-Headers | 실제 요청의 추가 헤더 |
반대로 프리플라이트 요청에 대한 답변에서도
필요한 값들이 있을 것이다 그것은 다음과 같다.
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
응답으로 돌아와야 하는 것 중 중요한것은 총 네개입니다.
각각의 의미는 다음과 같은데..
Access-Control-Allow-Origin | 서버 측 허가 출처 |
Access-Control-Allow-Methods | 서버 측 허가 메서드 |
Access-Control-Allow-Headers | 서버 측 허가 헤더 |
Access-Control-Max-Age | Preflight 응답 캐시 기간 |
이때 PREFLIGHT RESPONSE가 가져야하는 특징도 있는데
1. 응답 코드는 200번대여야한다.
2. 응답 바디는 비어 있는 것이 좋다.
🤢Simple Request
Simple Request는 Preflight 요청 없이 바로 요청을 하는 것을 말합니다.
다음 조건을 모두 만족해야 심플리퀘스트를 보내는게 가능한데
1. GET , POST , HEAD 메서드 중 하나여야한다.
2. Content-Type이 다음 3가지 중 하나여야한다.
application/x-www-form-urlencoded |
multipart-form-data |
text/plain |
3. 헤더는 다음 나열에 포함되는 것만 허용된다.
Accept |
Accept-Language |
Content-Language |
Content-Type |
요 모든 조건들을 만족해야 프리플라이트를 생략하고
심플 리퀘스트를 보낼 수 있다라는 뜻
이라고 생각을 할 수 있지만
🤮근데 왜 굳이 프리플라이트 해야함?
걍 심플리퀘스트 하면 서로 편한거아님??
이 프리플라이트는 CORS 설정을 제공하지 않는 서버에 접근할 때
클라이언트 -> 브라우저 -> 서버 -> 브라우저 -> 클라이언트
로 돌아오는 흐름에서 서버에 ALLOW-ORIGIN헤더가 없기때문에
브라우저는 CORS 에러를 서버에게 응답을 받은 시점에서 알게됩니다.
따라서 프리플라이트를 통해 미리 에러를 반환할 수 있다면..??
정말 좋겠다..
물론 이런 프리플라이트도 장점만은 있는 것은 아닌데
또한 예비 요청을 통해 보안을 강화하는 것은 좋지만 요청을 두번이나 보내게되면
요청에 대한 실제 응답을 받는 시간이 늘어나게 되어서 성능에도 영향을 미치게됩니다.
따라서 브라우저 캐시를 통해
Access-Control-Max-Age | Preflight 응답 캐시 기간 |
여기에 시간을 명시해주면 Preflight 요청을 캐싱시킬 수 있어서
최적화가 가능해진다. 요 시간이 남아있는 동안엔 프리플라이트를 매번하는게 아니라는 뜻이겠네요
또한 CORS 출처 비교와 차단은 서버가 아니라 브라우저가 한다는 것도 유의해야합니다.
출처를 비교하는 로직은 브라우저에 구현되어 있는 스펙이기 때문에
CORS 에러는 서버의 응답 문제가 아니라는 것입니다.
물론 앞서 말했듯 서버단에서
(Access-Control-Allow-Origin) 정보를 잘 줬다면 브라우저도 에러를 안냈겠지만...
아무튼 서버는 잘못 없음
또한 브라우저가 차단을 한다는 것은 브라우저를 통하지 않으면 CORS 에러도 발생하지 않는다는 것입니다.
따라서 브라우저를 통하지 않는 서버 - 서버의 통신에서는 정책이 적용되지않으니
CORS의 해결방법으로 프록시서버를 이용하는 방법도 존재합니다.
😨요청 방식에 따라 CORS 발생 여부도 다르다.
Cross-Origin 정책을 지원하는 태그 | Same-Origin 정책을 따르는 태그 |
<link> 다른 사이트의 .css 리소스에 접근 ssap가능 | XMLHttpRequest |
<img> 다른 사이트의 .png, .jpg같은 이미지에 접근 가능 | Fetch API |
<script> 다른 사이트의 .js 리소스 접근가능(단 모듈은안됨) |
그동안 잘 가져다 쓰던 태그들은 다 크로스오리진정책을 지원해줘서
괜찮았던 건가??!
😣브라우저의 CORS 동작 단계
1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달합니다.
Origin | 요청의 출처 |
아까 프리플라이트 요청 볼때 클라측에서 이거 넣어 보내줘야한다고 했던거 기억이 나네요
2. 서버는 응답 헤더에 Access-Control-Allow-Origin을 담아 클라에 전달합니다.
Access-Control-Allow-Origin | 서버 측 허가 출처 |
요거도 기억이 나네요
3. 브라우저는 클라이언트의 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교합니다.
유효하다면 다른 출처의 리소스를 문제 없이 가져올 수 있고
유효하지않다면 CORS 에러를 내뱉습니다.
고 과정에서 갑자기 뇌절이오면서 궁금했던건데 지피티한테 물어보니 일단 맞다고하네요
😭프리플라이트 예비 요청 캐시 매커니즘
1. 브라우저는 Preflight 요청을 할 때마다 먼저 Preflight 캐시를 확인해서 요청에 대한 캐시가 있는지 확인함
2. 응답이 캐싱되어있지 않으면 인증 절차를 밟음
3. 서버로부터 만약 Access-Control-Allow-Origin 얘가 포함된 응답헤더를 받으면
그 기간 동안 브라우저 캐시에 결과 저장
4. 다시 요청을 보내고 만일 응답이 캐싱 되어 있다면 Preflight를 보내는게 아니라 캐시된 응답을 사용함
듣기만해서는 되게 당연한 매커니즘이네요
🤢인증된 요청(Credentialed Request)
인증된 요청은 클라에서 서버에게 자격인증 정보를 실어 요청할 때 사용되는 요청입니다.
자격 인증 정보는 세션 ID가 저장되어있는 쿠키 / authorization 헤더에 설정하는 토큰 값
등을 말합니다. 요런 민감한 인증정보들은 따로 관리를 해줄 필요가 있다는 거군요
이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 것이 credentials 옵션이라는 것!
아까 prefilght할때는 OPTIONS를 사용했는데 여기서는 credentials를 쓰는군요
same-origin(디폴트값) | 같은 출처 간 요청에만 인증 정보 담기 가능 |
include | 모든 요청에 인증 정보 담기 가능 |
omit | 모든 요청에 인증 정보 담지 않음 |
요 credentials 옵션을 설정을 잘 해주지 않으면 쿠키와 같은 인증정보는 서버에게 전송되지 않습니다.
fetch("https://example.com:1234/users/login", {
method: "POST",
credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
body: JSON.stringify({
userId: 1,
}),
})
axios.post('https://example.com:1234/users/login', {
profile: { username: username, password: password }
}, {
withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})
코드 출처
이렇게 어떤 메서드를 쓰냐에 따라서 credentials를 지정하는 문법이 다르니까
필요할때 검색해보는 습관을 가져야겠네요.. 외않되
그래서 이렇게 CORS의 세가지 시나리오를 다 학습했고
어케 해야하는지도 알았고...
해결하는 방법도 대충 감이 잡히는데
CORS는 애초에 SOP의 장점인 보안을 제한적으로 뚫어주는 방법이니 그만큼 보안취약점이 생길것이다.
따라서 이것들을 해결할 방법도 있다.... 근데 조금 이미 너무 힘드니까 그건 다음에하자..
🐶참고자료
https://www.youtube.com/watch?v=-2TgkKYmJt4
'Network' 카테고리의 다른 글
www.google.com을 검색하면 무슨일이 생길까 (1) | 2023.04.20 |
---|---|
HTTP를 찍먹 해보자. (0) | 2023.04.01 |
RESTful API와 REST 성숙도 모델 (4) | 2023.03.29 |
json-server와 postman으로 REST API 실습 (3) | 2023.03.14 |
TCP와 UDP의 특징과 차이점 (0) | 2022.11.22 |