[FE] About Handling CORS Error

yongkini ·2022년 7월 11일
1

FE

목록 보기
6/7

About Handling CORS Error

SOP and CORS

: SOP(Same Origin Policy)에 대한 일종의 관용책(?) 요즘 같이 다른 origin을 가진 사이트 간에 request & response 통신이 많은 시대(?)에 맞춰 CORS Policy가 나왔다. 즉, SOP만 있던 시절에는 사실상 같은 origin 이 아니면 브라우저 상에서는 통신이 아예 불가능했지만, cors policy가 등장하여 오히려 그러한 strict 한 policy에 특정 조건만 만족한다면 통신을 허용하도록 해준거라고 볼 수 있다.

Why should we bypass cors?

: 하지만, 여전히 SOP는 개발자를 귀찮게 하고 있다. 물론 보안상의 문제를 브라우저 단에서 해결해준다는 점에서 매우 감사하지만, 아까 말했듯이 브라우저 단에서(개발자로 치면 프론트엔드 단에서) 다른 브라우저 혹은 open api를 사용할 때 브라우저가 자동으로 cors를 적용해서 block 하는 경우가 많기 때문에 이를 bypass 즉, 우회하는 방법에 대해 알아봐야할 필요성이 있다. 이번 포스팅에서는 그러한 부분에 대해서 알아보고자 한다.

Way to bypass cors

  • 간단하게 Same Origin이 동일하면 bypass 가능(당연한 것이지만 어쨌든 방법중 하나이다)
  • Same Origin 이 아니더라도 특정 api를 제공하는 쪽에서 'access-control-allow-origin'에 request 하는쪽의 Origin을 적어뒀거나 || * (모든 origin 허용) 해뒀다면 bypass 가능
  • middleware 즉, request를 보내는 client측과 그 request를 보내면서 preflight 요청을 함께 보내는 browser 그리고 그 요청들을 받고, 실제 data를 보내주는 server 사이에 틈이 있다면 middleware를 만들어서 브라우저를 통하지 않고 request를 보내는 방법이 있다.
    • proxy 서버를 거쳐서 api 요청을 해서 받아온다.
    • backend 서버를 직접 구축해서(cors 는 브라우저 단에서 체크하는 것이므로 서버에서 서버로 request를 하는 부분에 있어서는 cors 에러가 발생하지 않음) data를 받아오고, 그 data를 다시 클라이언트로 보내준다(nginx를 이용하는 방법도 여기에 해당됨).
  • nginx와 같은 리버스 프록시 서버를 구축하여 cors 세팅을 해줘서 우회한다(사실상 위의 방법과 거의 유사함)
    preflight 요청에서 개발자의 로직 변경으로는 cors를 막을 수 없다. 결정적으로 cors를 우회하려면 'Origin’을 건드려야하는데 이는 브라우저가 정한다.
  • preflight 요청을 보내지 않으면 cors에 걸리지 않을까라는 생각에 simple request(preflight 요청을 보내지 않는 요청)을 조사해봤으나 preflight를 보내지 않는다는 점만 같을 뿐 cors 정책은 적용하여 체크함.
  • simple request에 더하여 ‘credential request’ 에 대해서 알아봤으나 인증관련된 헤더를 포함할 때 서버의 header 설정에 제약을 두기위해 쓰는 request라고 한다(오히려 보안이 더 강해지는 request).
  • 과거 cors가 나오기 전까지는 JSONP(script 태그를 사용해 sop를 우회하여 JSON 데이터를 가져오기 위한 방법)라는 것을 사용해서 sop를 우회하여 cross-origin을 시도했다고 하는데, cors 등장 이후 거의 쓰지 않으며, 결정적으로 서버에서 response를 json 파일 형태로 내려줘야만 사용 가능하기 때문에 text/html 형식을 받아서 써야하는 상황에서는 사용 불가 + 보안 문제

타 서비스(slack, mattermost) 사례 조사

: 채팅 관련해서 cors를 우회하고자 해당 포스팅을 진행하고 있는 것이기에 다른 플랫폼에서는 채팅창에 cross-origin url의 데이터를 받고자 할 때 어떻게 cors를 우회하고 있는지 리서치해봤다.

Mattermost

: Mattermost는 서버단에서 open graph 태그 등의 정보를 추출하여 JSON 형태의 데이터로 가공하여 client로 보내주는 식으로 하고 있다. 그 과정을 살펴보면, 아래와 같다.

Step1 : input에 url을 치고, enter를 누르면(실제 post하면)

{
    "id": "8m5j9ndcgtrabrskjyyz9cqyqy",
    "create_at": 1657500676690,
    "update_at": 1657500676690,
    "edit_at": 0,
    "delete_at": 0,
    "is_pinned": false,
    "user_id": "ac46731hybbh9xbe3wqw35pkzw",
    "channel_id": "hf4sor57obf8tqmi48kjbasjna",
    "root_id": "",
    "parent_id": "",
    "original_id": "",
    "message": "https://www.hankyung.com/international/article/2022070971247",
    "type": "",
    "props": {
        "disable_group_highlight": true
    },
    "hashtags": "",
    "pending_post_id": "ac46731hybbh9xbe3wqw35pkzw:1657500676615",
    "reply_count": 0,
    "last_reply_at": 0,
    "participants": null,
    "metadata": {
        "embeds": [
            {
                "type": "opengraph",
                "url": "https://www.hankyung.com/international/article/2022070971247",
                "data": {
                    "type": "article",
                    "url": "https://www.hankyung.com/international/article/2022070971247",
                    "title": "피습 상황 VIP에 장막쳤어야…'경호 실패' 아베 사망으로 몰았나",
                    "description": "피습 상황 VIP에 장막쳤어야…'경호 실패' 아베 사망으로 몰았나, 이미나 기자, 국제",
                    "determiner": "",
                    "site_name": "한경닷컴",
                    "locale": "",
                    "locales_alternate": null,
                    "images": [
                        {
                            "url": "https://img.hankyung.com/photo/202207/01.30575849.1.jpg",
                            "secure_url": "",
                            "type": "",
                            "width": 630,
                            "height": 351
                        }
                    ],
                    "audios": null,
                    "videos": null
                }
            }
        ],
        "images": {
            "https://img.hankyung.com/photo/202207/01.30575849.1.jpg": {
                "width": 630,
                "height": 351,
                "format": "jpeg",
                "frame_count": 0
            }
        }
    }
}

위와 같은 데이터를 response로 받는다. “metadata” 키값을 보면 동영상 관련 메타 데이터를 렌더링하기 위한 정보가 왔음을 볼 수 있다(서버 단에서 open graph 태그를 활용해 데이터를 추출하여 보내는 것이고, 이 서버는 mattermost 자체 서버).

Step2 : 해당 데이터를 받아와서 바로 채팅 화면에 렌더링 해준다.

결과적으로 매터모스트는 자체적으로 백엔드 서버를 구축해놓고 urfurl 메서드를 만들어서 이에 대해 처리 해주는 방식으로 cors 우회를 하고 있다.

Slack

: slack 도 mattermost와 유사한 방식을 사용하는데 그 과정을 살펴보면, 아래와 같다.

Step1 : 채팅 input 창에 url을 타이핑하면 onChange 이벤트로 이를 감지하여 바로 api를 보낸다(url 링크인 경우).

Request URL : ‘https://nomadcoders.slack.com/api/chat.unfurlLink?_x_id=20e9b58f-1657459364.433&_x_csid=kUvOG19RNO4&slack_route=T60TDKNJK&_x_version_ts=1657321045&_x_gantry=true&fp=a1(자체 서버 api)’

위와 같은 URL로 POST 요청을 먼저 보내서 아래와 같은 데이터를 받는다.

{
    "ok": true,
    "attachments": {
        "https:\/\/www.youtube.com\/watch?v=-Y9VtoPvtuM": {
            "from_url": "https:\/\/www.youtube.com\/watch?v=-Y9VtoPvtuM",
            "service_name": "YouTube",
            "service_url": "https:\/\/www.youtube.com\/",
            "title": "Pink Sweat$ - Honesty [Official Music Video]",
            "title_link": "https:\/\/www.youtube.com\/watch?v=-Y9VtoPvtuM",
            "author_name": "Pink Sweats",
            "author_link": "https:\/\/www.youtube.com\/c\/PinkSweats",
            "fallback": "YouTube Video: Pink Sweat$ - Honesty [Official Music Video]",
            "thumb_url": "https:\/\/i.ytimg.com\/vi\/-Y9VtoPvtuM\/hqdefault.jpg",
            "thumb_width": 480,
            "thumb_height": 360,
            "video_html": "<iframe width=\"400\" height=\"225\" src=\"https:\/\/www.youtube.com\/embed\/-Y9VtoPvtuM?feature=oembed&autoplay=1&iv_load_policy=3\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen title=\"Pink Sweat$ - Honesty [Official Music Video]\"><\/iframe>",
            "video_html_width": 400,
            "video_html_height": 225,
            "service_icon": "https:\/\/a.slack-edge.com\/80588\/img\/unfurl_icons\/youtube.png",
            "id": 1,
            "original_url": "https:\/\/www.youtube.com\/watch?v=-Y9VtoPvtuM"
        }
    }
}

위와 같은 메타 데이터를 받는다. iframe 태그까지 string으로 받아와서 실제 enter를 쳐서 postMessage를 할경우에 필요한 정보를 미리 response로 받되 local storage에 저장한다(redux-persist를 쓰는 것일수도 있다).

Step2 : 실제로 엔터를 쳐서 메시지를 입력 완료하면
Request URL : ‘https://nomadcoders.slack.com/api/chat.postMessage?_x_id=20e9b58f-1657458817.473&_x_csid=t_iJLoBmVkA&slack_route=T60TDKNJK&_x_version_ts=1657321045&_x_gantry=true&fp=a1’ 로 POST 요청을 보낸다(자체 서버 api). 이와 동시에 local storage에 있던 정보를 바탕으로 실제 채팅 화면에 채팅 내용(유튜브 url 링크를 올린 경우 제목이랑 유튜브 마크 등이 렌더링됨)이 렌더링 된다.

Step3 : 이 때, 썸네일 이미지는 약간의 딜레이가 이뤄진 다음에 렌더링이 되는데, step2 api를 호출한 직후에 또다른 api를 날린다. 결과적으로

Request URL : https://slack-imgs.com/?c=1&o1=wi720.he540.si&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FvSfDUh78RsQ%2Fhqdefault.jpg 이 요청을 통해(GET) 썸네일 이미지를 받아온다. 이 url에서 파라미터 중 url 부분의 ‘https://i.ytimg.com/’ 는 유튜브 썸네일을 받을 수 있는 링크이고, 예상이지만 o1 파라미터로 width, height 정보를 줘서 원하는 사이즈로 크롭시켜서 오는 것 같다. 추가로, 이 api 역시 ‘https://slack-imgs.com/’ 라는 자체 서버를 이용해서 쓰고 있음.

결과적으로 슬렉도 unFurl 이라해서 api를 만들어서 쓰고 있었고, 대략적인 플로우를 정리해보면 먼저, 채팅 내용을 빨리 보여주기 위해 링크를 input에 치기만해도 제목, 썸네일 link, iframe 태그(string) 등을 받아온다(response)로 ⇒ 그 다음에 실제로 enter를 치면 이전에 받아온 정보로 렌더링을 하고, 썸네일을 위한 api를 또 호출해서 이미지를 크롭해서 받아온다 라고 추측할 수 있다.
** 추가적으로, 슬렉은 독특하게 같은 url 링크를 또 올리려고 하면(시간을 정해놓고) 아예 blocking을 해버리는 로직을 만들어놨다.

결론

: 결론적으로 cors를 우회하려면 꽤나 다양한 방법이 있지만, 본인이 하고 있는 프로젝트 상황에 맞춰서 적용하되 대부분 서버를 거치거나 프록시 서버를 거쳐서 해결한다는 점을 알고 넘어가자.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글