악명 높은 CORS에 대한 이해
악명 높은 CORS에 대한 이해

악명 높은 CORS에 대한 이해

Tags
Node.js
Web Dev
Published
January 23, 2024
Author
gozneokhan

CORS(Cross Origin Resource Sharing)란?

CORS는 교차 출처에서의 리소스 공유를 안전하게 제어하는 정책입니다. 브라우저 기반의 웹 애플리케이션에서 다른 출처의 리소스 접근을 보안적으로 제한하는데 사용됩니다.
⚠️
Warning !
Access to fetch at ‘https://myhompage.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

출처(Origin) 란?

URL은 일반적으로 하나의 문자열처럼 보이지만, 실제로는 여러 구성 요소로 이루어져 있습니다. 이러한 구성 요소는 다음과 같습니다.
notion image
구성 요소
예시
설명
Protocol (Scheme)
http, https
통신에 사용되는 프로토콜
Host
사이트 도메인
웹 사이트의 도메인 주소
Port
80, 443, 3000 등
통신에 사용되는 포트 번호
Path
/user/profile
사이트 내부의 경로
Query String
?key1=value1&key2=value2
요청에 포함된 매개변수(key)와 값(value)들
Fragment
#section1
문서 내의 특정 섹션을 가리킴(해시 태그)
이 중에서 CORS를 이해하는 데에는 Protocol + Host + Port가 중요합니다.
출처(Origin)는 프로토콜, 호스트, 포트를 모두 합친 것을 의미합니다. 이 출처가 서로 다를 경우, 브라우저에서는 보안상의 이유로 특정 리소스에 대한 접근을 제한합니다.
간단한 자바스크립트를 사용하여 현재 페이지의 출처를 알아낼 수도 있습니다.
console.log(location.origin); // "https://www.example.com" (포트 번호 80번은 생략됨)

동일 출처 정책 (Same-Origin Policy)

Same-Origin Policy (SOP)

SOP은 동일 출처에서만 리소스를 공유할 수 있도록 하는 정책입니다. 다른 출처의 리소스와의 상호작용은 허용되지 않습니다.

동일 출처 정책이 필요한 이유

SOP는 보안 문제로 출처가 다른 어플리케이션 간에 자유로운 소통을 막아, 해커의 CSRF나 XSS와 같은 공격으로부터 개인 정보를 보호합니다.

같은 출처와 다른 출처 구분 기준

SOP는 출처의 동일함을 기준으로 합니다. 동일 출처를 판단하는 요소는 URL의 Protocol(Scheme), Host, Port 세 가지입니다.
URL
동일 출처 여부
이유
https://www.domain.com:3000/about
O
프로토콜, 호스트, 포트 번호 동일
https://www.domain.com:3000/about?username=inpa
O
프로토콜, 호스트, 포트 번호 동일
http://www.domain.com:3000
X
프로토콜 다름 (http ≠ https)
https://www.another.co.kr:3000
X
호스트 다름
https://www.domain.com:8888
X
포트 번호 다름
https://www.domain.com
X
포트 번호 다름 (443 ≠ 3000)
요약하면, 같은 프로토콜, 호스트, 포트를 사용하면 동일 출처로 간주되며, 하나라도 다르면 브라우저에서 차단됩니다.

출처 비교와 차단은 브라우저가 수행

SOP는 브라우저에서 동작하며, 서버가 응답을 처리한 후 브라우저는 해당 응답을 분석하여 동일 출처 정책을 위반하면 에러를 발생시킵니다. 이로써 브라우저는 다른 출처로의 불필요한 접근을 차단하여 보안을 강화합니다.

브라우저 설정에서 SOP 비활성화에 대한 방법

브라우저의 SOP를 우회하려면 클라이언트 코드가 아닌 서버 코드를 활용할 수 있습니다. 서버 단에서 다른 출처로의 API 요청을 보내면 CORS 에러를 피할 수 있습니다. 프록시(Proxy) 서버 사용이 이 방법 중 하나입니다.

교차 출처 리소스 공유 CORS (Cross-Origin Resource Sharing)

CORS는 다른 출처 간 리소스 공유를 관리하는 정책으로, 출처 간 상호 작용을 허용하는 보안 규칙을 정의합니다. 이는 웹 어플리케이션이 안전하게 다른 출처의 리소스를 사용할 수 있도록 예외를 제공합니다.

CORS 에러와 SOP 정책

CORS 에러는 브라우저의 Same-Origin Policy (SOP) 때문에 발생합니다. SOP는 다른 출처의 리소스 접근을 제한하는데, 이를 극복하기 위해 CORS가 사용됩니다. CORS는 SOP를 우회하여 다른 출처의 리소스를 허용합니다.

CORS 정책 준수하며 SOP 정책 회피하기

  1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달
      • 웹은 HTTP 프로토콜을 사용하여 서버에 요청을 보냅니다. 브라우저는 요청 헤더에 Origin 필드에 출처를 함께 담아 보냅니다.
  1. 서버는 응답 헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달
      • 서버가 요청에 대한 응답을 할 때 응답 헤더에 Access-Control-Allow-Origin 필드를 추가하고 '이 리소스를 접근하는 것이 허용된 출처 URL'을 값으로 넣어 클라이언트에 전달합니다.
  1. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교
      • 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 차단 여부를 결정합니다. 유효하지 않다면 해당 응답을 사용하지 않고 CORS 에러가 발생합니다.

CORS 해결책은 서버의 허용이 필요

CORS 에러를 해결하는 핵심은 서버에서 클라이언트 출처를 허용하는 설정을 하는 것입니다. 백엔드 개발자는 서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 명시하여 클라이언트의 요청을 허용해야 합니다. 클라이언트에서는 브라우저의 보안 정책으로 인해 이 헤더를 변경할 수 없습니다.

CORS 작동 방식 3가지 시나리오

예비 요청 (Preflight Request)

브라우저에서 요청을 보낼 때, 실제 요청을 보내기 전에 브라우저는 예비 요청을 먼저 서버에 보냅니다. 이를 "Preflight Request"라고 하며, 이 예비 요청은 본 요청의 안전성을 확인하기 위한 것입니다. 주로 OPTIONS라는 HTTP 메소드를 사용하며, 자바스크립트의 fetch() 메서드를 통한 API 요청 예시를 설명하겠습니다.
  1. 브라우저에서의 예비 요청 보내기
      • 브라우저는 서버로 OPTIONS 메소드로 예비 요청을 보냅니다.
      • 이때, Origin 헤더에는 자신의 출처를, Access-Control-Request-Method 헤더에는 실제 요청에 사용할 메소드를, Access-Control-Request-Headers 헤더에는 실제 요청에 사용할 헤더들을 설정합니다.
  1. 서버에서의 예비 요청 응답
      • 서버는 예비 요청에 대한 응답으로 다음과 같은 헤더 정보를 브라우저로 전송합니다.
        • Access-Control-Allow-Origin: 허용되는 Origin들의 목록을 설정합니다.
        • Access-Control-Allow-Methods: 허용되는 메소드들의 목록을 설정합니다.
        • Access-Control-Allow-Headers: 허용되는 헤더들의 목록을 설정합니다.
        • Access-Control-Max-Age: 해당 예비 요청이 브라우저에 캐시될 수 있는 시간을 초 단위로 설정합니다.
  1. 브라우저에서의 본 요청 전송
      • 브라우저는 서버의 응답을 기반으로 예비 요청과 본 요청의 안전성을 비교합니다.
      • 안전하다고 판단되면 본 요청을 보냅니다.
  1. 서버에서의 본 요청 응답
      • 서버는 본 요청에 대한 응답을 생성하고 브라우저로 전송합니다.
  1. 자바스크립트에서의 데이터 처리
      • 브라우저는 받은 본 요청의 응답 데이터를 자바스크립트로 넘겨줍니다.

개발자 도구에서 예비 요청 확인하기

브라우저의 개발자 도구를 활용하여 API 요청의 예비 요청과 관련된 통신을 확인할 수 있습니다. 아래는 fetch() 메서드를 사용한 API 요청의 예시입니다.
await fetch("http://localhost:4000/users/location-registration", {"method":"DELETE"})

예비 요청 헤더 및 응답 확인

  • 요청 헤더에는 Origin과 관련된 정보가 포함되며, 응답 헤더에는 CORS 관련 정보인 Access-Control-Allow-Origin 등이 포함됩니다.
  • 두 헤더의 URL 값이 일치하면 CORS가 허용되어 정상 응답을 받을 수 있습니다. 다른 출처의 경우 CORS 정책 위반으로 인한 에러가 발생할 수 있습니다.

예비 요청의 문제점과 캐싱

  • 예비 요청은 보안을 강화하지만, 실제 요청에 소요되는 시간이 증가하여 성능 저하가 발생합니다.
  • Access-Control-Max-Age 헤더를 사용하여 예비 요청을 캐싱함으로써 성능 최적화가 가능합니다.

예비 요청 캐시 설정

  • Access-Control-Max-Age 헤더를 사용하여 브라우저에 캐시된 결과를 설정된 기간 동안 사용합니다.
  • 캐시된 결과를 이용하면 서버에 대한 예비 요청을 반복해서 보내지 않아 성능이 향상됩니다.

예시 코드

// 예비 요청(Preflight Request)을 포함한 API 요청 fetch("http://localhost:4000/users/location-registration", { method: "DELETE", headers: { "Content-Type": "application/json", "Authorization": "Bearer your_access_token" } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error("Error:", error));

단순 요청 (Simple Request)

단순 요청은 예비 요청(Preflight) 없이 바로 서버에 본 요청을 보내는 방식입니다. 서버는 응답 헤더에 Access-Control-Allow-Origin를 포함하여 브라우저에서 CORS 정책 위반 여부를 확인합니다.

단순 요청을 사용할 수 있는 조건

  1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 합니다.
  1. 특정 헤더들(Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width) 중 일부만 사용될 때 해당합니다.
  1. Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 합니다.

주의사항

  • 대부분의 API 요청은 JSON 등의 형식으로 통신하므로 Content-Type이 특정 조건을 만족하지 않아 예비 요청이 발생하는 경우가 많습니다.
  • 대부분의 API 요청은 예비 요청(preflight)을 수반하는 것이 일반적이며, 단순 요청이 발생하는 경우는 제한적입니다.

예시 코드

// 단순 요청의 예시 (POST 메소드, Content-Type이 text/plain) fetch("http://example.com/api/data", { method: "POST", headers: { "Content-Type": "text/plain" }, body: "Hello, server!" }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error("Error:", error));

인증된 요청 (Credentialed Request)

인증된 요청은 클라이언트에서 서버로 요청을 보낼 때 자격 인증 정보(Credential)를 함께 전송하는 경우를 말합니다. 이는 주로 세션 ID가 저장된 쿠키(Cookie)나 Authorization 헤더에 담긴 토큰과 같은 인증 정보를 포함합니다. 인증된 요청은 CORS의 세 가지 요청 중 하나로, 클라이언트가 다른 출처의 서버로 인증 정보를 함께 전송할 때 사용됩니다.

클라이언트에서 인증 정보를 보내도록 설정하기

본적으로 브라우저의 요청 API들은 인증과 관련된 데이터를 요청 데이터에 담지 않도록 설정되어 있습니다. 이때, 요청에 인증 정보를 담을 수 있게 해주는 옵션이 credentials입니다. 이 옵션은 세 가지 값을 가질 수 있습니다.
  • same-origin (기본값): 같은 출처 간 요청에만 인증 정보를 담을 수 있습니다.
  • include: 모든 요청에 인증 정보를 담을 수 있습니다.
  • omit: 모든 요청에 인증 정보를 담지 않습니다.
// fetch 메서드 fetch("https://example.com:1234/users/login", { method: "POST", credentials: "include", // 인증 정보를 공유하겠다는 설정 body: JSON.stringify({ userId: 1, }), })
// axios 라이브러리 axios.post('https://example.com:1234/users/login', { profile: { username: username, password: password } }, { withCredentials: true // 인증 정보를 공유하겠다는 설정 })
// jQuery 라이브러리 $.ajax({ url: "https://example.com:1234/users/login", type: "POST", contentType: "application/json; charset=utf-8", dataType: "json", xhrFields: { withCredentials: true // 인증 정보를 공유하겠다는 설정 }, success: function (retval, textStatus) { console.log(JSON.stringify(retval)); } });

서버에서 인증된 요청에 대한 헤더 설정하기

서버도 이러한 인증된 요청에 대해 일반적인 CORS 요청과는 다르게 대응해야 합니다.
  • 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 합니다.
  • 응답 헤더의 Access-Control-Allow-Origin 값에 와일드카드 문자()는 사용할 수 없습니다.
  • 응답 헤더의 Access-Control-Allow-Methods 값에 와일드카드 문자()는 사용할 수 없습니다.
  • 응답 헤더의 Access-Control-Allow-Headers 값에 와일드카드 문자()는 사용할 수 없습니다.
즉, 응답의 Access-Control-Allow-Origin 헤더가 와일드카드(*)가 아닌 분명한 Origin으로 설정되어야 하고, Access-Control-Allow-Credentials 헤더는 true로 설정되어야 합니다. 그렇지 않으면 브라우저의 CORS 정책에 의해 응답이 거부됩니다. (인증 정보는 민감한 정보이기 때문에 출처를 정확하게 설정해주어야 합니다)
만일 이를 어기면 아래와 같은 CORS 에러 메시지를 접할 수 있습니다.
  • Access-Control-Allow-Credentials 설정을 하지 않았을 경우
  • Access-Control-Allow-Origin가 로 설정되어 있을 경우
참고로 인증된 요청도 예비 요청(preflight)이 먼저 발생합니다. 위의 그림에서는 단순 GET 요청이기 때문에 예비 요청은 생략되었습니다.

CORS를 해결하는 방법

Chrome 확장 프로그램 이용

Chrome 브라우저에서는 CORS(Cross-Origin Resource Sharing) 문제를 해결하기 위한 확장 프로그램을 사용할 수 있습니다. 'Allow CORS: Access-Control-Allow-Origin'이라는 크롬 확장 프로그램을 설치하면 됩니다. 해당 확장 프로그램을 설치하면 브라우저 오른쪽 상단에서 확장 프로그램을 활성화할 수 있습니다. 이를 활성화하면 로컬 환경에서 API를 테스트할 때 CORS 문제를 빠르게 해결할 수 있습니다.

프록시 사이트 이용하기

heroku 프록시 서버

javascriptCopy code const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경 fetch(`https://cors-anywhere.herokuapp.com/${url}`) .then((response) => response.text()) .then((data) => console.log(data));

cors proxy app 프록시 서버

htmlCopy code <script src='https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js'></script> <script> axios({ url: 'https://cors-proxy.org/api/', method: 'get', headers: { 'cors-proxy-url': 'https://google.com/' // 이 부분을 이용하는 서버 URL로 변경 }, }).then((res) => { console.log(res.data); }) </script>

cors.sh 프록시 서버

javascriptCopy code const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경 fetch(`https://proxy.cors.sh/${url}`) .then((response) => response.text()) .then((data) => console.log(data));

서버에서 Access-Control-Allow-Origin 헤더 설정하기

직접 서버에서 HTTP 헤더를 설정하여 출처를 허용하는 것이 가장 정석적인 해결책입니다. 다양한 서버에 대한 설정 방법을 예시로 제시하였습니다.

Node.js 세팅

javascriptCopy code var http = require('http'); const PORT = process.env.PORT || 3000; var httpServer = http.createServer(function (request, response) { // Setting up Headers response.setHeader('Access-Control-Allow-origin', 'https://inpa.tistory.com'); // 특정 출처만 허용 response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // 모든 HTTP 메서드 허용 response.setHeader('Access-Control-Allow-Credentials', 'true'); // 쿠키 주고받기 허용 // ... response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end('ok'); }); httpServer.listen(PORT, () => { console.log('Server is running at port 3000...'); });

Express.js 세팅

javascriptCopy code const express = require('express') const cors = require("cors"); const app = express(); // ... app.use(cors({ origin: "https://naver.com", // 접근 권한을 부여하는 도메인 credentials: true, // 쿠키 인증 요청 허용 optionsSuccessStatus: 200, // 응답 상태 200으로 설정 }));

AWS S3 호스팅 세팅

AWS S3에서는 S3 콘솔에서 해당 버킷의 권한(Permissions) 탭에서 CORS 설정을 할 수 있습니다. 설정은 JSON 형식으로 작성되며 출처 및 허용 메서드 등을 설정할 수 있습니다.
jsonCopy code [ { "AllowedHeaders": ["Authorization"], "AllowedMethods": ["GET", "HEAD"], "AllowedOrigins": ["http://www.example.com"], "ExposeHeaders": ["Access-Control-Allow-Origin"] } ]

글을 마치며

CORS(Cross-Origin Resource Sharing)는 웹 애플리케이션에서 Same-Origin Policy(SOP)로 인해 제한되는 보안 정책을 극복하기 위한 교차 출처 리소스 공유 정책입니다.
Same-Origin Policy는 브라우저에서 웹 페이지가 특정 출처에서 로드된 리소스에만 접근할 수 있도록 제한하는 보안 정책입니다. 그러나 현대적인 웹 애플리케이션에서는 다양한 출처의 데이터나 서비스에 접근해야 하는 경우가 빈번하게 발생합니다. 이를 위해 CORS가 도입되었으며, 클라이언트가 다른 출처의 리소스에 안전하게 접근할 수 있도록 규칙을 정의합니다.
CORS는 주로 HTTP 헤더를 활용하여 동작합니다. 클라이언트가 다른 출처로 HTTP 요청을 보낼 때, 요청 헤더에는 현재 페이지의 출처를 나타내는 Origin이 담겨집니다. 서버는 이 Origin을 확인하고, 응답 헤더의 Access-Control-Allow-Origin에 해당 출처를 허용하는 값을 설정합니다. 브라우저는 이 두 값이 일치할 때에만 해당 리소스를 로드하고 사용합니다.
CORS의 필요성은 다른 출처의 리소스를 안전하게 활용하기 위함입니다. 예를 들어, 웹 페이지에서 외부 도메인에 이미지를 불러오거나 서드파티 API에 데이터를 요청하는 상황에서, 브라우저의 보안 정책을 준수하면서도 리소스를 안전하게 공유할 수 있도록 하는 것이 주된 목적입니다.
요약하면, CORS는 Same-Origin Policy의 출처 제한을 극복하여, 웹 애플리케이션이 다른 출처의 리소스에 안전하게 접근할 수 있도록 하는 교차 출처 리소스 공유 정책입니다.

Reference