CORS
고친 사람 github-actions[bot]
CORS 는 다른 주소에서 받아 온 응답을 페이지의 코드가 읽어도 되는지 서버에게 확인받습니다. 브라우저는 서버가 허락한다고 답한 때에만 그 응답을 코드에 넘깁니다. 허락이 없으면 응답이 도착했더라도 읽히지 않고 버려집니다.
쉽고 빠른 이해
CORS 는 「이 사이트가 내 응답을 읽어도 되는가」를 서버에게 물어 두는 절차입니다. 쇼핑몰 페이지의 코드가 그 회사의 주문 서버를 부를 때처럼, 주소의 앞머리가 서로 다르면 이 확인이 걸립니다.
확인이 없으면 곤란한 일이 생깁니다. 로그인해 둔 사람이 남의 사이트를 열기만 해도, 그 사이트의 코드가 은행 페이지를 몰래 불러 내용을 읽어 갈 수 있습니다.
어떻게 도는가:
- 브라우저가 요청에 「나는 어느 사이트다」를 적어 보냅니다
- 서버는 「그 사이트는 읽어도 된다」를 응답에 적어 돌려줍니다
- 브라우저가 둘을 맞춰 보고, 맞을 때만 응답을 페이지 코드에 넘깁니다
대가도 있습니다. 요청 모양에 따라 브라우저가 미리 한 번 더 물어보므로 왕복이 늘어납니다. 그리고 검사하는 쪽이 브라우저라, 브라우저를 안 거치는 호출에는 이 보호가 걸리지 않습니다.
상세
사무실 건물 로비를 떠올려 봅시다. 방문객이 아무 층에나 올라갈 수는 없고, 경비가 그 층에 전화를 걸어 「이 사람 올려 보내도 됩니까」를 먼저 묻습니다. 된다는 답이 와야 방문객이 엘리베이터를 탑니다.
CORS 는 Cross-Origin Resource Sharing, 교차 출처 리소스 공유의 줄임말입니다. 브라우저가 다른 출처의 응답을 페이지 코드에 넘기기 전에 서버의 허락을 확인하는 규칙이고, 그 허락은 요청과 응답에 붙는 몇 줄의 헤더로 오갑니다. 헤더는 본문 앞에 붙어 이 메시지를 어떻게 다룰지 알리는 줄입니다.
허락을 주는 쪽은 서버이고, 허락을 확인해 응답을 넘기거나 버리는 쪽은 브라우저입니다. 이 둘이 나뉘어 있다는 것이 아래 모든 대목의 바탕입니다.
같은 출처와 다른 출처
출처는 주소 앞머리의 세 조각을 묶은 이름입니다. https 처럼 맨 앞에 오는 스킴, 그다음의 호스트,
그리고 포트가 그 셋이고, 셋이 모두 같아야 같은 출처입니다.
https://shop.example/a // 기준 페이지
https://shop.example/b // 같은 출처
http://shop.example/a // 다른 출처 · 스킴
https://api.shop.example // 다른 출처 · 호스트
https://shop.example:8443 // 다른 출처 · 포트
한 조각만 달라도 다른 출처입니다. 회사 도메인이 같고 같은 팀이 굴리는 서버여도, 앞에 붙은 이름이 다르면 브라우저에게는 남입니다.
브라우저에는 동일 출처 정책이 먼저 깔려 있습니다. 어느 페이지의 코드가 다른 출처의 응답을 읽지 못하게 막아 두는 기본 규칙입니다.
이 규칙이 없으면 남의 사이트를 열어 둔 것만으로 곤란해집니다. 그 사이트의 코드가 내가 로그인해 둔 은행 페이지를 불러 내용을 읽어 갈 수 있기 때문입니다. CORS 는 그 정책을 걷어 내는 것이 아니라, 서버가 허락한 만큼만 예외를 여는 절차입니다.
허락이 오가는 한 왕복
브라우저는 다른 출처로 나가는 요청에 Origin 헤더를 붙입니다. 이 값은 요청을 낸 페이지의
출처이고, 페이지 코드가 바꿔 쓸 수 없습니다.
서버는 응답에 Access-Control-Allow-Origin 헤더를 실어 누구에게 열어 줄지 적습니다. 브라우저는
그 값을 요청의 Origin 과 맞춰 보고, 맞을 때만 응답을 페이지 코드에 넘깁니다.
Origin: https://shop.example // 요청이 밝힌 출처
Access-Control-Allow-Origin: * // 누구에게나 연다
별표는 출처를 가리지 않고 연다는 뜻입니다. 특정 출처 하나만 적을 수도 있고, 그때는 요청마다
Origin 을 보고 답을 달리 만들어야 합니다.
sequenceDiagram
participant 페이지
participant 브라우저
participant 서버
페이지->>브라우저: 다른 출처로 요청해 달라
브라우저->>서버: 요청 · Origin 을 붙여서
서버-->>브라우저: 응답 · Access-Control-Allow-Origin
Note over 브라우저: 두 값을 맞춰 본다
브라우저-->>페이지: 맞으면 응답을 넘긴다
Note over 브라우저,페이지: 안 맞으면 버리고 오류만 알린다
그림에서 서버까지 다녀오는 것은 한 번입니다. 검사는 응답이 브라우저에 도착한 다음에 일어납니다.
미리 허락을 받아 오는 요청
어떤 요청은 보내기 전에 한 번 더 물어봅니다. 웹 페이지의 폼으로도 보낼 수 있던 모양의 요청은 그냥 나갑니다. 폼은 CORS 가 생기기 전부터 다른 출처로 보낼 수 있었고, 그것까지 막으면 옛 페이지가 깨지기 때문입니다.
그 밖의 요청은 다릅니다. 지우는 요청 메서드를 쓰거나 Authorization 같은 헤더를 코드가 직접
붙인 요청은 서버의 데이터를 바꿀 수 있습니다. 브라우저는 이런 요청을 먼저 보내지 않고 허락부터
받아 옵니다. 이렇게 미리 묻는 요청을 프리플라이트 요청이라고 합니다.
프리플라이트 요청은 OPTIONS 메서드로 나갑니다. 무엇을 하려는지를 Access-Control-Request-Method
와 Access-Control-Request-Headers 에 적어 알립니다.
서버는 Access-Control-Allow-Methods 와 Access-Control-Allow-Headers 로 답합니다. 허락이
떨어져야 브라우저가 진짜 요청을 보냅니다. 같은 물음을 매번 되풀이하지 않도록,
Access-Control-Max-Age 에 그 답을 얼마 동안 재사용해도 되는지 초 단위로 적을 수 있습니다.
sequenceDiagram
participant 브라우저
participant 서버
브라우저->>서버: OPTIONS · 이 메서드를 써도 되나
서버-->>브라우저: 허락하는 메서드와 헤더
Note over 브라우저: 허락 밖이면 여기서 끝난다
브라우저->>서버: 진짜 요청
서버-->>브라우저: 응답 · Access-Control-Allow-Origin
왕복이 둘로 늘어납니다. 서버가 허락을 얼마 동안 기억해도 되는지 답해 두면, 그 사이에 나가는 요청들은 다시 한 왕복으로 끝납니다.
로그인 정보가 실리는 요청
쿠키처럼 로그인 상태를 나르는 값을 자격 증명이라고 부릅니다. 이런 값은 다른 출처로 가는 요청에 기본으로 안 붙고, 붙이려면 페이지 코드가 자격 증명을 함께 보내겠다고 켜야 합니다.
서버도 따로 허락해야 합니다. 응답에 Access-Control-Allow-Credentials 를 참으로 적어야 브라우저가
그 응답을 페이지 코드에 넘깁니다.
이때는 Access-Control-Allow-Origin 에 별표를 쓸 수 없고 출처를 하나 적어야 합니다. 누구에게나
연다는 표시와 로그인한 사람의 데이터를 같이 두면, 어느 사이트든 그 사람 이름으로 그 데이터를 읽어
갈 수 있게 되기 때문입니다.
요청이 아니라 응답을 막는 검사
CORS 검사는 응답이 돌아온 다음에 일어납니다. 프리플라이트를 안 거치는 요청은 이미 서버에 닿았고 서버는 그것을 처리했습니다. 브라우저가 버리는 것은 응답을 읽을 권리지 서버가 한 일이 아닙니다.
그래서 CORS 는 크로스 사이트 요청 위조 방어가 아닙니다. 그 공격은 응답을 안 읽어도 요청이 도착하는 것만으로 성립합니다. 그것을 막으려면 서버 쪽 방어가 따로 필요합니다.
브라우저 밖에서 보내는 요청에도 CORS 는 걸리지 않습니다. 서버끼리 부르는 호출이나 명령줄 도구는 이 규칙을 지킬 의무가 없습니다. 검사하는 쪽이 브라우저이기 때문입니다.
서버 쪽에서 자주 빠뜨리는 대목
응답이 페이지 코드에 넘어가도 코드가 읽을 수 있는 응답 헤더는 몇 개로 제한됩니다. 목록 총 개수를
따로 실어 보내는 헤더처럼 직접 만든 헤더는, 서버가 Access-Control-Expose-Headers 에 그 이름을
적어야 코드가 읽습니다.
프리플라이트 요청에는 자격 증명이 안 실립니다. OPTIONS 요청까지 로그인 검사를 걸어 두면 허락을
받기도 전에 막혀, 진짜 요청은 나가 보지도 못합니다.
응답을 캐싱하는 중간 장비도 살펴야 합니다. 서버가 요청의 Origin 을 보고 답을 달리 만든다면
응답에 Vary: Origin 을 적어, 캐시가 출처별로 따로 보관하게 해야 합니다. 이것을 빠뜨리면 한
출처에게 준 허락이 담긴 응답이 다른 출처에게 그대로 나갑니다.
관련 항목
CORS 검사에서 오가는 헤더 필드
Origin · Access-Control-Allow-Origin · Access-Control-Allow-Methods · Access-Control-Allow-Headers · Access-Control-Allow-Credentials · Access-Control-Expose-Headers · Access-Control-Max-Age · Vary
CORS 가 예외를 여는 대상인 기본 제약
동일 출처 정책 · 콘텐츠 보안 정책 · 샌드박스 · 혼합 콘텐츠 · 사이트 격리
CORS 요청이 실려 나가는 규약과 메서드
HTTP · 요청 메서드 · OPTIONS · GET · POST · 헤더 · 프리플라이트 요청 · 상태 코드
CORS 확인을 거치는 브라우저 기능
Fetch · XMLHttpRequest · WebGL · 캔버스 · 웹 폰트 · DOM
CORS 로는 못 막는 공격
크로스 사이트 요청 위조 · 교차 사이트 스크립팅 · 클릭재킹 · 세션 하이재킹
허락을 나르거나 가로채는 중간 장비
프록시 · 리버스 프록시 · CDN · 게이트웨이 · 캐싱
요청에 자격 증명을 실어 주는 값
쿠키 · 세션 · 인증 · 자격 증명 · Authorization
다른 이름: Cross-Origin Resource Sharing · 교차 출처 리소스 공유 · 교차 출처 자원 공유