웹사이트 패킷을 프록시로 잡아서 확인해보면 아래와 같이 OPTIONS 메소드를 계속 사용하는 곳이 있는데 이유가 뭘까? 라는 생각이 들어 공부하고 정리하게 되었다.
⇒ CORS(Cross-Origin Resource Sharing) 요청과 관련이 있다.
OPTIONS 메소드를 사용하는 이유
웹사이트에서 OPTIONS 메소드를 사용하는 이유는 주로 CORS(Cross-Origin Resource Sharing) 요청과 관련이 있다.
CORS는 웹 애플리케이션이 다른 도메인에서 리소스를 요청할 수 있게 해주는 보안 기능이다.
OPTIONS 메소드의 주요 목적은 서버에 미리 요청을 보내어, 실제 요청을 보내기 전에 서버가 허용하는 메소드, 헤더, 그리고 기타 정보들을 확인하는 것이며, 이는 "preflight" 요청이라고도 불린다.
OPTIONS 메소드를 사용하는 주요 이유
- CORS Preflight 요청:
- 브라우저는 클라이언트가 수행하려는 실제 요청 전에 서버에 OPTIONS 메소드를 사용하여 "preflight" 요청을 보낸다.
- 이 요청은 서버가 실제 요청을 허용할지 여부를 확인하기 위한 목적으로 사용된다.
- 서버는 OPTIONS 요청에 대해 응답하면서, 어떤 메소드들이 허용되는지, 어떤 헤더들이 사용할 수 있는지, 그리고 클라이언트가 어느 도메인에서 접근할 수 있는지를 명시한다.
- 클라이언트와 서버 간의 통신 설정 확인:
- OPTIONS 메소드는 서버가 특정 리소스에 대해 어떤 HTTP 메소드들을 지원하는지 알 수 있게 해준다.
- 이로 인해 클라이언트는 잘못된 메소드로 요청을 보내는 것을 방지할 수 있다.
- API 탐색:
- API를 설계할 때, 클라이언트가 어떤 작업을 할 수 있는지 알아보기 위해 OPTIONS 메소드를 사용하여 탐색할 수 있다.
요청 패킷:
OPTIONS /resource HTTP/1.1
Host: api.example.com
응답 패킷:
HTTP/1.1 200 OK
Allow: GET, POST, PUT, DELETE, OPTIONS
지금 우리가 살펴볼 부분은 첫번째인 CORS Preflight 요청을 보면 된다.
CORS(Cross-Origin Resource Sharing)란?
CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 현재 도메인과 다른 도메인에 있는 리소스에 접근할 수 있도록 해주는 보안 기능이다.
*Origin : URL에서 프로토콜, 도메인, 포트 번호를 합친 부분
ex) Origin의 경우 window.location.origin 로 알아낼 수 있다.
URL : https://domain.com:443/posts/page?data=text
Origin : https://domain.com:443
웹 브라우저는 보안을 위해 동일 출처 정책인 SOP(Same-Origin Policy)를 적용하고 있다.
SOP의 경우 스크립트가 자신이 로드된 도메인 외의 다른 도메인에 있는 리소스에 접근하지 못하도록 막는 보안 정책이다.
예를 들어 https://domain.com 에서 로드된 스크립트는 https://another-domain.com 의 리소스에 접근할 수 없다.
즉, CORS의 경우 적절한 헤더를 설정하여 어떤 도메인에서, 어떤 메소드로, 어떤 헤더를 사용할 수 있는지 명시하여 클라이언트가 안전하게 자원에 접근할 수 있도록 SOP에 의해 막힌 정책을 풀어주는 것이라고 볼 수 있다.
CORS 작동 방식
CORS는 서버가 특정 도메인, 메소드, 헤더에 대한 요청을 허용할지 여부를 브라우저에게 알려줌으로써 동작한다.
CORS를 사용하기 위해서는 서버에서 특정 HTTP 헤더를 설정하여 브라우저에 허용된 도메인, 메소드, 헤더 등을 명시해야 한다.
Simple Request 패킷 예시
브라우저가 특정 조건을 만족하는 요청을 보내면, 서버는 단순히 허용된 출처를 응답 헤더에 포함시켜 응답한다.
클라이언트 요청:
GET /resource HTTP/1.1
Host: api.example.com
Origin: http://example.com
서버 응답:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com
Preflight 요청 패킷 예시
좀 더 복잡한 요청(예: ‘POST’, 특정 헤더 사용)은 브라우저가 실제 요청을 보내기 전 OPTIONS 메소드를 사용하여 서버의 허용 여부를 확인한다.
CORS 요청의 경우, 만약 클라이언트가 도메인 A에서 도메인 B로 데이터를 요청하려고 한다면, 브라우저는 다음과 같은 절차를 따른다.
Preflight 요청 (OPTIONS):
OPTIONS /resource HTTP/1.1
Host: api.example.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
서버 응답:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
(실제 응답값 패킷을 확인해보면 아무 내용이 없다.)
실제 요청 (예: POST):
POST /resource HTTP/1.1
Host: api.example.com
Origin: http://example.com
Content-Type: application/json
이와 같이 OPTIONS 요청은 보안과 서버의 능력을 확인하며, 특히 웹 애플리케이션의 보안을 유지하고, 서버와 클라이언트 간의 올바른 상호작용을 보장하는 데 매우 유용하다.
주요 CORS 헤더
앞에서 살펴본 패킷 예시 내 Access-Control로 시작하는 헤더들 중 주요로 사용되는 것들은 아래와 같다.
- Access-Control-Allow-Origin: 요청을 허용할 도메인을 지정한다. 예를 들어, ‘*’는 모든 도메인에서의 요청을 허용하며, 특정 도메인을 지정할 수도 있다.
- Access-Control-Allow-Methods: 허용되는 HTTP 메소드를 지정한다. 예를 들어, GET, POST, PUT 등이 있다.
- Access-Control-Allow-Headers: 허용되는 요청 헤더를 지정한다. 예를 들어, Content-Type 등이 있다.
- Access-Control-Max-Age: Preflight 요청에 대한 응답을 캐시할 시간을 초 단위로 지정한다.
- Access-Control-Allow-Credentials: 클라이언트가 자격 증명(쿠키, 인증 헤더 등)을 포함할 수 있도록 허용한다.
실제 패킷 분석
아래 패킷을 보면 OPTIONS 메소드 이후 GET, POST, DELETE 메소드들의 패킷이 오는 것을 확인할 수 있다.
위의 이미지를 보면 요청패킷과 응답패킷 내용을 확인할 수 있다.
요청패킷의 경우 OPTIONS 메소드를 사용하여 패킷을 보내며, Origin 주소로 DELETE 메소드를 요청시키고 있다.
응답패킷은 요청받은 Origin 주소와 DELETE 메소드를 허용하는 응답을 보낸다.
위에서 확인한 OPTIONS 요청/응답 패킷을 주고받은 이후 DELETE 패킷이 요청된 것을 확인할 수 있다.
일반적인 OPTIONS 요청과 CORS의 Preflight 요청 차이점
- 용도:
- OPTIONS 요청: 서버가 특정 리소스에 대해 허용하는 HTTP 메소드를 알아내기 위한 일반적인 HTTP 요청.
- Preflight 요청: CORS와 관련된 요청으로, 브라우저가 클라이언트에서 실제 요청을 보내기 전에 서버에 사전 요청을 보내어 해당 요청이 허용되는지 확인하기 위한 것.
- 헤더:
- OPTIONS 요청: 일반적으로 추가적인 CORS 관련 헤더가 필요하지 않다.
- Preflight 요청: Origin, Access-Control-Request-Method, **Access-Control-Request-Headers**와 같은 CORS 관련 헤더가 포함된다.
- 적용 범위:
- OPTIONS 요청: CORS와 관련이 없으며, 서버의 메소드 지원 정보를 얻기 위해 사용된다.
- Preflight 요청: CORS와 직접적으로 관련되며, 클라이언트가 다른 도메인에서 자원을 요청할 때 사용된다.
CORS를 구현하기 위해 필요한 것
CORS(Cross-Origin Resource Sharing)를 구현하려면 서버와 클라이언트 측에서 몇 가지 설정과 조치를 취해야 한다.
서버 측 설정
서버 측에서 CORS를 구현하는 데 필요한 주요 요소는 HTTP 응답 헤더이다.
이 헤더들은 클라이언트가 특정 조건 하에서 서버의 리소스에 접근할 수있도록 허용한다.
주요 CORS 헤더는 다음과 같다:
- Access-Control-Allow-Origin:
- 이 헤더는 서버가 특정 도메인에서의 요청을 허용할지 여부를 지정한다.
- 예를 들어, 특정 도메인(https://example.com)을 허용하거나 모든 도메인(*)을 허용할 수 있다.
Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Origin: *
- Access-Control-Allow-Methods:
- 이 헤더는 서버가 허용하는 HTTP 메소드(GET, POST, PUT, DELETE 등)를 명시한다.
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
- Access-Control-Allow-Headers:
- 이 헤더는 서버가 허용하는 커스텀 헤더나 표준 헤더를 지정한다.
Access-Control-Allow-Headers: Content-Type, Authorization
- Access-Control-Allow-Credentials:
- 이 헤더는 자격 증명(쿠키, HTTP 인증 등)을 포함한 요청을 허용할지를 지정한다.
Access-Control-Allow-Credentials: true
- Access-Control-Max-Age:
- 이 헤더는 preflight 요청의 결과를 브라우저가 캐시할 시간을 초 단위로 지정한다.
Access-Control-Max-Age: 86400
- Access-Control-Expose-Headers:
- 이 헤더는 브라우저가 응답에 포함된 특정 헤더에 접근할 수 있도록 허용한다.
Access-Control-Expose-Headers: Content-Length, X-Kuma-Revision
클라이언트 측 설정
클라이언트 측에서는 일반적으로 추가적인 설정이 필요하지 않지만, 자격 증명을 포함한 요청을 할 경우 다음과 같이 설정할 수 있다:
- Credentials 포함 요청:
- 자격 증명을 포함한 요청을 하기 위해서는 withCredentials 옵션을 true로 설정해야 한다.
fetch('https://api.example.com/data', { method: 'GET', credentials: 'include' })
서버 측 예시 - Node.js (Express)
Node.js와 Express를 사용하여 CORS를 설정하는 예시이다:
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: 'https://example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
app.get('/resource', (req, res) => {
res.json({ message: 'This is CORS-enabled for only example.com.' });
});
app.listen(3000, () => {
console.log('CORS-enabled web server listening on port 3000');
});
서버 측 예시 - Django
Django에서 CORS를 설정하는 예시이다:
django-cors-headers 패키지를 설치한다:
pip install django-cors-headers
Django 설정 파일(settings.py)을 수정한다:
INSTALLED_APPS = [
...,
'corsheaders',
]
MIDDLEWARE = [
...,
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
CORS_ALLOWED_ORIGINS = [
"https://example.com",
]
CORS_ALLOW_METHODS = [
"GET",
"POST",
"PUT",
"DELETE",
]
CORS_ALLOW_HEADERS = [
"content-type",
"authorization",
]
CORS_ALLOW_CREDENTIALS = True
CORS_PREFLIGHT_MAX_AGE = 86400
결론적으로는 CORS 구현 시 서버에서 적절한 HTTP 헤더를 설정하여 클라이언트의 요청을 허용할 도메인, 메소드, 헤더 등을 명시해야 한다.
클라이언트 측에서는 일반적으로 특별한 설정이 필요하진 않지만, 자격 증명을 포함하는 경우 추가 설정이 필요할 수 있다.
잠재적인 보안 위험
- 잘못된 CORS 설정:
- Access-Control-Allow-Origin 헤더를 와일드카드(*)로 설정하면, 모든 도메인에서 리소스에 접근할 수 있게 되어 보안 위험이 커진다.
- Access-Control-Allow-Credentials를 true로 설정하고 Access-Control-Allow-Origin에 와일드카드를 사용하면 보안 취약점이 발생할 수 있다.
- 크로스 사이트 스크립팅(Cross-Site Scripting, XSS):
- 악의적인 사용자가 XSS 공격을 통해 피해자의 브라우저에서 악의적인 스크립트를 실행하여 CORS 요청을 통해 민감한 정보를 외부 서버로 전송할 수 있다.
- 사용자 정보 유출:
- CORS 설정이 잘못되면, 악의적인 웹사이트가 사용자의 인증 정보를 포함한 요청을 다른 도메인으로 전송하여 민감한 데이터를 탈취할 수 있다.
보안 강화 방법
- 정확한 도메인 허용:
- Access-Control-Allow-Origin 헤더에 와일드카드(*)를 사용하지 말고, 정확히 허용할 도메인만을 명시한다.
Access-Control-Allow-Origin: <https://trusted-domain.com>
- 자격 증명 허용 설정:
- 자격 증명을 포함한 요청을 허용하려면, Access-Control-Allow-Credentials 헤더를 true로 설정하고, 정확한 도메인을 Access-Control-Allow-Origin 헤더에 명시한다.
Access-Control-Allow-Origin: <https://trusted-domain.com> Access-Control-Allow-Credentials: true
- 엄격한 헤더 및 메소드 제한:
- Access-Control-Allow-Methods와 Access-Control-Allow-Headers 헤더를 사용하여 허용할 HTTP 메소드와 헤더를 제한한다.
Access-Control-Allow-Methods: GET, POST Access-Control-Allow-Headers: Content-Type, Authorization
- Preflight 요청 캐싱 설정:
- Access-Control-Max-Age 헤더를 설정하여 preflight 요청의 결과를 브라우저가 캐싱할 수 있게 한다.
Access-Control-Max-Age: 3600
- 입력 검증 및 방어적 프로그래밍:
- 서버에서 입력 값을 검증하고, 잠재적인 XSS 공격을 방지하기 위한 방어적 프로그래밍 기법을 사용한다.
- 보안 툴 및 모니터링:
- CORS 설정을 정기적으로 검토하고, 보안 툴을 사용하여 취약점을 검사하며, 로그 및 모니터링 시스템을 통해 의심스러운 활동을 감시한다.
end.