일단, 역시나 람다 사용 중에 이번에는 프론트엔드단에서 axios로 백엔드에 요청 보내는 중에 CORS 오류가 났다. 나는 /POST 메서드를 API 엔드포인트에서 허용하여 작업하고 있었는데,

Internal Server Error (500) 이 떴다.
나는 분명히 POST 메서드를 허용해주고, CORS 헤더들도 모두 다 알맞게 설정해주었건만. 콘솔을 열고, 무언가를 확인해보니...

엥 왜 두번써지지?
엥 왜 두번써지지?

알아보니, /POST, /PUT 등의 메서드는 리소스 최적화를 위해 preflight라는 걸 /OPTIONS 메서드로 백엔드단에 미리 보내는 기작이 있었다!
그래서 /OPTIONS 메서드를 허용해주고 그 /OPTIONS product를 간단히
Header: "Access-Control-Allow-Origin": "*"
Body: HTTPStatus.OK
를 리턴하는 람다 함수를 또 만들어줘야 하는 거였다...
왜 그런가 깊게 알아보자...
여기서 잠깐! CORS가 뭐냐구요?
Cross-Origin Resource Sharing(CORS).
서로 다른 출처에서 온 정보 간의 공유에 있어서, 보안상의 이유로 그냥은 공유되게 못하겠다는 뜻이다.
좀 더 깊게 알아보면, 이 CORS는 브라우저 상에서 적용되는 보안사항이다.
뭐 파이썬 같은 거에서 아무데나 api 요청 날려도 CORS 오류를 보기 힘들고 유난히 웹 개발을 하면 자주 볼 수 있는 이유도 바로 이 때문이다.
브라우저는 SOP 정책을 따른다.
Same-origin Policy의 약자인데, 기본적으로 웹 브라우저에서 구동되는 프로그램은 해당 위치에 있는 리소스에만 접근할 수 있다는 것이다.
그래서, 일단 기본적으로는 브라우저는 만약 localhost에서 돌아갈 시 내 로컬에 있는 에셋이나 코드만 참조할 수 있다. 일반적으로 개발 서버를 로컬로 설정하여 디버깅하는 npm start 같은 것들이 이런 예시이다.
어딘가에서 배포되고 있는 웹도 마찬가지다. 배포되고 있는 주소에 있는 html이나 에셋만 참조할 수 있는 것이다.
그런데, 개발을 하다 보면 브라우저에서 다른 곳에 API 요청을 보내야 할 때가 많다. 특히 요즘 같은 Client-side Rendering 코딩이 유행하는 시점에서는 API가 REST API로 대체되고 정말 필요한 정보만 백엔드에서 받아오는 경우가 많다.
이렇게 다른 출처에 있는 정보를 브라우저에서 동시에 처리하려면, CORS 정책을 따라야 한다.
그래서 이 CORS라는 게 어떤 식으로 작동하냐.
즉, 서버단에서 접근 가능한 출처를 명시해줌으로써 CORS 정책을 만족할 수가 있다.
보안에는 안전하지 않지만 일반적으로 localhost 개발단계에서 백엔드 API 서버와 연결할 때는 백엔드단에서 Access-Control-Allow-Origin": "*" 헤더를 넣어줌으로써 모든 CORS 오류를 해결하는 경우가 많다. 하지만 이는 보안에 강건하지 않으므로 실제 deploy 단계에서는 출처를 명시해주는 것이 좋다.
쉽게 얘기해서, 브라우저 상단에 치는 아주 긴 것들이 출처라고 볼 수 있다.

다만, 이 중에서 출처를 결정짓는 요소는 3가지인데, 바로 프로토콜, 도메인, 포트이다.
도메인 주소와 포트는 컴퓨터 내부 네트워크 카드에서 IP주소와 포트처럼 하나의 주소 역할을 한다고 보면 되고, 프로토콜은 바로 http, https 이런 것들이다.
프로토콜은 통신 프로토콜을 이야기하는 것으로, 통신 방법을 이야기한다. ssh도 그 중 하나가 될 수 있는 것이다. 일반적으로 통신 프로토콜에 따라 포트번호도 달라지므로, 프로토콜이 바뀌면 포트도 일반적으로 바뀐다.
아무튼, 이렇게 프로토콜, 도메인, 포트가 같은 출처는 같은 출처라고 인식하게 된다.
http://localhost:3000과 비교해봤을 때,
| 출처 | 같은 출처인가? |
|---|---|
http://localhost:3000/mypage | ✅ 프로토콜, 도메인, 포트가 같음 |
https://localhost/mypage | ❌ 프로토콜, 포트가 다름 (https 443 포트로 자동설정) |
http://localhost:5000/mypage2 | ❌ 포트가 다름 |
https://localhost2:3000/ | ❌ 프로토콜, 도메인이 다름 |
맞긴 해.
사실 개발하다 보면 이것만큼 진짜 별 거 아닌 것 같은데 사람 짜증나게 하는 게 없다. 세상에 해커들이 없었더라면... ㅂㄷㅂㄷ
흠... 근데 찾아보니까 SOP 정책이 국제적 표준으로 시행된지 얼마 안 됐다. 2011년, 국제 인터넷 표준화 기구에서 관리하는 기술 표준 RFC(Request for Comments)에서 처음 제안된 정책이라고 하는데, 아무래도 2008년 옥션 개인정보 유출 사건의 여파 때문일까?
2008년 옥션 개인정보 유출은 CSRF 공격 사례로 꼽힌다. CSRF 공격이 무엇인지는 나중에 설명하겠다. 아무튼 이 CSRF 공격이든, XSS 공격이든, 이 웹 애플리케이션이라는 것이 소스 코드를 누구나 브라우저에서 볼 수 있고 URL 엔드포인트 구조 등을 쉽게 유추할 수 있기 때문에 보안에 매우 취약할 수밖에 없다.
코딩 공부 좀 했다 하는 사람들이나 컴퓨터에 관심 많다 하는 사람들, 어릴 적 URL에 장난쳐가면서 페이지 바꾸고 쿼리스트링 바꿔보면서 코딩에 재미 붙인 사람도 많지 않은가.
즉, 보안에 취약할 수밖에 없는 웹 애플리케이션의 특성상 이 CORS는 필연적인 흐름이다...
그래서, CORS 정책을 구축하지 않으면 어떤 대참사가 일어나는지 한번 보자.
CSRF 공격은 관리자가 특정 행동을 발생시키는 URL을 억지로 실행하게 만들어서 관리자가 원치 않는 행동을 하도록 만드는 방법이다.
일반적으로 HTML img 태그의 URL은 GET 요청으로 실행된다. 그런데 만약 관리자 비밀번호를 바꾸는 API 요청의 주소가 http://changePassword.com/user.do?what=user_pw_change&id={바꿀유저이름}&to={바꿀비밀번호}라면, 그리고 이것이 GET 요청이라면, 관리자가 저 URL을 실행하는 순간 비밀번호가 바뀌어버린다.
이를 악용한 것이 CSRF 공격이다. 서버에 요청하는 API 엔드포인트의 URL을 유추하여 그것을 관리자의 페이지에서 자동으로 실행하게 만들어버리는 것이다.
그래서 메일 등에 저런 URL을 가진 HTML img 태그를 넣고 관리자에게 메일을 보내면, 관리자가 메일을 여는 순간 GET요청이 실행되므로 비밀번호가 바뀌어버린다.
비밀번호 변경이라는 기작 자체는 브라우저에서 요청을 볼 수 있으므로, 해커가 직접 비밀번호를 변경하고 네트워크 요청을 읽어본 뒤 이대로 CSRF를 날렸는데 비밀번호가 변경될 수가 있다. 이런 방식으로 2008년에 옥션이 개털렸다고 한다. (...)
XSS 공격은 전자 게시판 등에 HTML의 구조를 변경할 수 있는 스크립트를 탈취하여 관리자의 세션이나 쿠키 등을 빼앗는 경우를 말한다.
예를 들면, 누군가가 게시판에
<HTMLTag>
<BlahBlah />
</HTMLTag>
라고 입력했고, Blah Blah에 쿠키를 탈취하여 해커의 컴퓨터로 전송하는 코드를 집어넣었다고 해보자. 그러면 그냥 탈취되어버리는겨~
그래서 이렇게 취약한 웹 특성상, 교차 출처 사용 시 메서드를 지정해서 허용한다거나, 허용된 주소에서만 교차 출처가 사용 가능하도록 하거나... CORS에서 지원하는 여러 가지 제한정책으로 보안 문제를 해결할 수 있다.
사실 CSRF 같은 경우도 POST로 body에 데이터를 넣도록 비밀번호 변경하는 요청을 짰으면 해킹하기 쉽지 않았을 것이다.
게다가 요즘은 JWT처럼 Authorization에서 안전한 방법들이 많이 활용되고 있으니...
자~~ 돌고 돌아 여기까지 왔다.
사실 CORS에 대한 설명은 이걸 설명하기 위한 초석이었다.
그래서 나는 분명히 AWS의 API Endpoint에서 CORS 설정을 해줬는데 왜 문제가 발생했을까?
그 KEY는 preflight에 있다.
이 preflight라는 것은, API request를 할 때 simple하게 request를 하는 게 아니라, preflight 요청을 보내어, /OPTIONS 메서드로 해당 서버에 CORS 정책에 대한 정보를 얻어오는 것이다.
예를 들어 데이터가 200MB나 하는 매우 큰 POST 요청이 있다고 해보자. 그런데 이 요청이 알고 보니 CORS 요청을 위반하는 요청이라면? 200MB 보내는 리소스 낭비가 극심할 것이다. 그래서, 클라이언트단에서 미리 OPTIONS 메서드로
나 손가락으로 너 쿡 찔러볼 건데 너 이정도는 괜찮니?
하고 물어보는 것이다. 여기서 YES 답장이 오면 원래 하려 했던 POST 메서드의 simple request를 실행한다.
모든 경우에서 그런 건지 잘은 모르겠지만 적어도 axios에서는 POST, PUT 메서드를 보낼 때 이런 OPTIONS preflight를 자동으로 보내는 것 같았다.
(생각해 보니 flask로 코드를 짤 때도 나는 분명 POST만 보냈는데 OPTIONS 요청이 서버단에 들어간 걸 볼 수가 있었다)
근데 나는 AWS에서 API Endpoint에 OPTIONS 메서드를 열어놓지도 않았고, 그에 해당하는 람다 함수도 연결해두지 않았으니 애초에 OPTIONS 요청에서 아무것도 응답받지 못해 문제가 생긴 거였다.
앞으로 람다 써서 POST 혹은 PUT 요청 보낼 꺼면 꼭! preflight 요청을 보낼 수 있는 상황인지 확인해보자.
내가 가벼운 프로젝트든 사업적 프로젝트든 그래도 이것저것 해보면서 이 오류를 처음 겪은 이유는, 람다를 처음 사용해서 그런 것 같다. 그렇다면 왜 다른 걸 사용했을 땐 괜찮았는데 람다를 사용했을 때 난 이 preflight가 문제가 될 수 있음을 처음 안 걸까?
flask_cors : flask에는 flask_cors라는 라이브러리가 있다.
여기서 cors 관련 문제를 자동으로 처리해주는데, flask에서 OPTIONS 메서드 요청이 POST 전에 항상 들어왔었던 걸 보면 flask_cors에서 자동으로 preflight를 처리해주는 것으로 보인다.
간단하다. 사진대학은 CORS 오류를 피하기 위해 nginx로 프록시 서버를 Amazon EC2에서 구현했기 때문.
사진대학은 Amazon S3에서 호스팅 중인 클라이언트단 웹앱을 가비아 도메인에 연결한 후, Amazon EC2에서 돌고 있는 flask 서버를 통해 연결했다. 그 과정에서 nginx 프록시 서버 또한 구축했는데, 그랬기 때문에 애초에 SOP 정책을 만족하여 이 오류를 볼 일이 없었던 것 같다.