CORS 문제

CORS 문제는 프론트엔드와 백엔드가 서로 다른 도메인에 호스팅될 때 발생한다. 예를 들어, 프론트엔드가 https://frontend.example.com에, 백엔드는 https://api.example.com에 호스팅되는 상황을 생각할 수 있다. 기본적으로 웹 브라우저는 보안상의 이유로 서로 다른 도메인 간의 요청을 제한한다.

CORS의 작동 방식

Preflight Request

  • 정의
    Preflight Request는 본 요청에 앞서 브라우저가 OPTIONS 메서드를 사용해 서버에 보내는 요청이다. 이 요청을 통해 브라우저는 서버가 클라이언트의 요청을 허용할지를 확인하고, 허용되는 경우 본 요청을 실행한다.

  • 응답 구성
    서버는 Response Headers를 포함해 요청을 허용할지를 설정한다. 주요 헤더는 다음과 같다:
    • Access-Control-Allow-Origin: 허용된 도메인 지정. 예: https://frontend.example.com 또는 *.
    • Access-Control-Allow-Methods: 허용된 HTTP 메서드 지정. 예: GET, POST, PUT, DELETE.
    • Access-Control-Allow-Headers: 허용된 헤더 지정. 예: Content-Type, Authorization.
  • 사용하는 경우
    교차 출처 요청(Cross-Origin Request)이며 비단순 요청인 경우 Preflight Request가 사용된다.

단순 요청과 비단순 요청

  • 단순 요청(Non-Simple Request) (Preflight Request 불필요)
    • HTTP 메서드가 GET, POST, HEAD일 때
    • 헤더가 CORS에서 허용하는 특정 기본 헤더들(Accept, Accept-Language, Content-Language, Content-Type 중 특정 값 등)만 포함될 때
    • Content-Typetext/plain, multipart/form-data, application/x-www-form-urlencoded일 때
  • 비단순 요청(Simple Request) (Preflight Request 필요)
    • PUT, DELETE, PATCH 또는 HEAD 같은 메서드를 사용할 때
    • 위의 조건 외의 헤더(Authorization 등)가 포함된 경우


단순요청인 경우 브라우저는 cors를 어떻게 확인하는가

단순 요청은 별도의 Preflight Request 없이 서버에 바로 도달하기 때문에, 브라우저는 서버가 이 요청을 허용할지 여부를 서버의 응답을 받은 후에야 알 수 있다. 단순 요청의 경우, 브라우저는 별도의 Preflight Request 없이 서버로 바로 요청을 전송한다. 서버 응답의 Access-Control-Allow-Origin 헤더를 통해 허용 여부를 확인한다.

단순 요청의 흐름은 다음과 같다:

  1. 요청 전송: 클라이언트(브라우저)가 단순 요청을 서버로 바로 전송합니다. 이때 Preflight Request는 발생하지 않는다.

  2. 서버의 응답 확인: 서버는 클라이언트의 요청을 받으면, 응답에 CORS 관련 헤더(Access-Control-Allow-Origin 등)를 포함해 허용된 출처와 메서드인지를 명시한다.

  3. 브라우저의 CORS 정책 검토: 브라우저는 서버의 응답에 포함된 CORS 헤더를 확인합니다. 서버가 응답에 Access-Control-Allow-Origin 헤더로 해당 출처를 명시하지 않으면 브라우저는 CORS 정책에 따라 이 응답을 차단합니다. 이 차단은 클라이언트 쪽에서 발생하므로, 네트워크 요청은 성공했지만 응답이 브라우저에 의해 보이지 않게 된다.


  • 동작 원리

    Preflight Request는 교차 출처 리소스를 안전하게 가져오기 위해 아래 절차로 동작한다:

    1. 사전 요청 전송: 클라이언트가 본 요청에 앞서 OPTIONS 메서드로 Preflight Request를 보낸다. 이 요청에는 클라이언트가 본 요청에서 사용할 메서드와 헤더 정보가 담겨 있다. 예를 들어, Access-Control-Request-Method: POST와 같은 방식으로 본 요청의 메서드가 포함된다.

    2. 서버의 응답 확인: 서버는 이 요청을 받고 응답에 CORS 관련 헤더를 포함해 허용 여부를 표시한다.
      • Access-Control-Allow-Origin: 요청을 허용할 출처를 지정한다.
      • Access-Control-Allow-Methods: 허용할 HTTP 메서드를 명시한다.
      • Access-Control-Allow-Headers: 허용할 사용자 정의 헤더를 명시한다.
      • Access-Control-Max-Age: Preflight Request의 응답을 캐시할 시간(초 단위)을 설정한다.
    3. 본 요청 전송: 서버가 허용하면, 클라이언트는 본 요청을 서버로 전송한다.


  • 왜 모든 요청에 대하여 Preflight Request가 발생하지는 않는가

    Preflight Request는 모든 요청에 발생하지 않는다. 단순한 요청이나 같은 출처로의 요청은 CORS의 영향을 받지 않기 때문에 사전 검사 없이 곧바로 요청을 보낼 수 있다. 이는 Preflight Request가 과도한 네트워크 트래픽을 발생시키지 않도록 설계된 것이기도 하다.

프론트엔드 서버와 CORS

프론트엔드 서버를 허가하면 HTML, CSS, JavaScript 파일들이 사용자의 브라우저에 로드되지만, 브라우저가 백엔드 서버에 요청을 보낼 때는 브라우저의 CORS 정책이 적용된다. 이는 악성 사이트가 사용자 대신 요청을 보내는 것을 방지하기 위함이다.

CORS 설정의 목적

백엔드 서버가 특정 프론트엔드 서버를 허용하도록 설정하면, 해당 프론트엔드 서버에서 로드된 자바스크립트가 백엔드 서버에 요청을 보낼 수 있다. 예를 들어, https://frontend.example.com에서 로드된 자바스크립트가 https://api.example.com에 요청할 수 있도록 하려면 백엔드의 응답 헤더에 Access-Control-Allow-Origin: https://frontend.example.com 설정이 필요하다.

유저가 직접 자원을 요청할 경우

사용자가 콘솔이나 도구를 통해 직접 백엔드 서버에 요청을 보내려 하면, 브라우저는 여전히 CORS 정책을 적용한다. 그러나 악의적인 사용자가 브라우저가 아닌 방법(cURL, Postman 등)으로 서버에 요청을 보내는 경우 CORS는 적용되지 않는다. 이를 방지하기 위해 백엔드에서 추가적인 인증 절차를 통해 출처를 검증해야 한다.

결론

CORS는 브라우저 보안 정책으로, 서버가 특정 도메인에서의 요청을 허용하는 설정이다. 이는 해당 프론트엔드 도메인에서 로드된 자바스크립트를 통해 백엔드에 요청을 보낼 수 있게 하며, 서버 간 직접 요청을 막기 위해 추가적인 보안 조치가 필요하다.


요청 흐름

  1. 사용자가 프론트엔드 페이지(https://frontend.example.com)에 접속한다.
  2. 프론트엔드 페이지의 자바스크립트가 백엔드 서버(https://api.example.com)로 요청을 보낸다.
  3. 요청은 Origin: https://frontend.example.com 헤더와 함께 전송된다.
  4. 백엔드 서버는 CORS 정책을 통해 요청을 허용할지를 결정한다.
  5. 허용된 경우 백엔드 서버는 응답 헤더에 Access-Control-Allow-Origin: https://frontend.example.com을 포함한다.

CORS 판단 대상

CORS 판단 대상은 요청을 보내는 사용자의 IP가 아닌, 요청의 출처(Origin)이다. 브라우저가 자동으로 이 출처를 Origin 헤더에 포함해 서버로 전송하므로, 유저의 IP는 CORS 판단에 영향을 주지 않는다.

자바스크립트 설정

자바스크립트 자체에는 특별한 설정이 필요하지 않다. 중요한 것은 다음과 같은 기본 흐름이다:

  1. 자바스크립트가 로드되는 출처: 자바스크립트는 특정 도메인에서 로드되며, 이 도메인이 Origin 헤더에 포함된다.
  2. 자바스크립트 요청: AJAX 요청(Fetch API, XMLHttpRequest 등)을 통해 백엔드 서버로 데이터를 요청한다.
  3. 브라우저 역할: 브라우저가 요청의 출처를 Origin 헤더에 포함해 전송한다.

백엔드의 CORS 설정 예시

// 예: Express.js를 사용하는 Node.js 서버의 CORS 설정
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
  origin: 'https://frontend.example.com', // 허용할 프론트엔드 도메인
  methods: 'GET,POST,PUT,DELETE', // 허용할 HTTP 메서드
  allowedHeaders: 'Content-Type,Authorization', // 허용할 헤더
};

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS 설정이 완료되었습니다.' });
});

app.listen(3000, () => {
  console.log('서버가 3000번 포트에서 실행 중입니다.');
});

위 설정은 https://frontend.example.com에서 로드된 자바스크립트가 백엔드에 요청을 보낼 수 있도록 한다.

결론

CORS 판단 대상은 자바스크립트가 로드된 출처(도메인)이며, 브라우저가 이 출처를 자동으로 포함해 요청을 보내므로 유저의 IP가 아닌 자바스크립트의 로드 도메인이 백엔드 CORS 정책에 따라 허용 여부를 결정한다. 자바스크립트 코드에는 특별한 설정이 필요 없으며, 백엔드의 CORS 설정이 중요하다.

CORS는 완벽한 보안 솔루션이 아니다

브라우저 환경에서의 CORS는 신뢰할 수 있는 도메인에서의 요청만을 허용하기 위한 방법이지만, 이는 보안의 한 측면일 뿐이다. 다음과 같은 이유로 CORS만으로는 충분하지 않다:

  • 클라이언트의 조작 가능성: 사용자가 브라우저 개발자 도구나 기타 방법을 통해 직접 요청을 수정할 수 있다. 이는 CORS 헤더를 조작하는 것을 포함한다.
  • 서버 간 요청: CORS는 브라우저에서의 제한이기 때문에, curl, Postman 같은 도구를 사용하여 서버 간 요청을 보낼 때는 적용되지 않는다.

추가적인 보안 조치

따라서 서버는 CORS 외에도 여러 가지 보안 조치를 추가로 구현해야 한다:

인증 및 권한 부여

  • JWT (JSON Web Token)나 OAuth 같은 인증 방식을 사용하여 사용자 인증을 강화한다.
  • 각 요청에 대해 사용자 인증 토큰을 확인한다.

CSRF 보호

  • CSRF (Cross-Site Request Forgery) 토큰을 사용하여 요청의 유효성을 검증한다.
  • 이는 사용자가 아닌 다른 출처에서 온 요청을 방지하는 데 도움이 된다.

입력 데이터 검증

  • 서버로 들어오는 모든 데이터는 검증하고, 예상하지 않은 입력이나 악성 입력을 필터링한다.

Rate Limiting

  • API 요청에 대해 속도 제한을 설정하여 악성 사용자가 서버를 과도하게 사용하는 것을 방지한다.

로그 및 모니터링

  • 비정상적인 활동을 모니터링하고, 의심스러운 요청을 기록한다.

결론

CORS는 클라이언트 측 스크립트가 서버에 접근하는 것을 제어하는 중요한 보안 메커니즘이지만, 이것만으로는 충분하지 않다. 서버는 인증, 권한 부여, 입력 검증, CSRF 보호, 로그 및 모니터링 등 다양한 보안 기법을 함께 사용하여 전체적인 보안을 강화해야 한다.