Web

SOP(동일 출처 정책) / CORS(교차 출처 리소스 공유)

개발가락 2022. 11. 30. 17:20

첫 글인 만큼 무엇을 쓸까 고민하다가 제가 웹 공부하면서 처음으로 가장 큰 좌절을 준 CORS에 대해서 이야기 해 보자고 합니다.

 

 

SOP / CORS Principle ( MDN )

 

 

SOP 란?

어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식.

 

여기서 출처(origin)란, 3가지 요소 (protocol , host , port)중 하나만 틀려도 다른 출처로 인식합니다.

ex) http:www.naver.com/hello/nested/dir

1. https:www.naver.com/hello/nested/dir - ( X ) : protocol mismatch

2. http:m.naver.com/hello/nested/dir - ( X ) : host mismatch

3. http:www.naver.com:80/hello/nested/dir = ( X ) : port mismatch

4. http:www.naver.com/wtf/nested/dir/123123 = ( O ) : SAME ORIGIN

 

 

왜 필요할까?

한가지 예시 상황을 들자면,

클라이언트가 A 사이트에 로그인 합니다.

로그인이 성공적일 경우, A사이트에서 클라이언트 측으로, 로그인 구현 방식에 따라 응답을 보냅니다.

그 후, 클라이언트가 A사이트에 방문할때마다, 요청 헤더에 인증관련 정보를 넣어서 보내게 됩니다.

 

만약 이 상황에서 클라이언트가 불순한 의도를 가진 사이트 B를 방문한다면 어떻게 될까요?

B에 접속 시, A사이트 내 사용자 정보를 조작하는 스크립트가 있다고하면, 공격당하게 됩니다(CSRF | XSRF).

 

CSRF Attack process

 

그렇기에, 적어도 사용자를 보호하기 위해서 SOP 정책이 생겨난 겁니다.

 

그렇다면 CORS 란?

추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 프로토콜

 

왜 필요할까?

 

SOP의 배경은 이제 알겠는데, 그렇다고 교차 출처 요청을 안 할 수도없는게, 이미 사용하는게 너무 흔하기에(소셜 로그인 등..), 교차 출처 요청을 특정 방식으로 요청하면 허용할 수 있겠금 하였는데, CORS 표준을 지킨 요청에 한해 허용하기로 하였습니다. SOP이 성벽이라면 CORS가 문지기 느낌으로 봐도 될거 같습니다.

 

다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 합니다.

 

시나리오

교차 출처 리소스 공유 시, 일어날 수 있는 시나리오가 크게 3가지 있습니다.

 

1. Simple Request(단순 요청) 

단순 요청의 경우, CORS는 사전 요청(preflight)을 하지 않습니다.(사전 요청의 경우 맨 밑에서 설명)

 

단순 요청에 속하려면 다음 3가지 조건을 만족해야합니다.

1) GET , HEAD , POST 중 하나

2) readableStream X

3) 자동으로 설정된 헤더만 있어야함 (자세한 설명은 공식문서에서 : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS)

  • Accept
  • Accept-Language
  • Content-Language 
  • ..
// app.tsx
const SERVER = 'http://localhost:8000';
function App() {
	const [responseMessage, setResponseMessage] = useState<string | null>();

	const fetchToServer = async () => {
		const response: AxiosResponse<string> | void = await axios.get(SERVER + '/simple').catch((e) => {
			console.log(e);
		});

		setResponseMessage(response?.data);
	};
	return (
		<div className="App">
			<Button onClick={fetchToServer}>Fetch!!</Button>
			<h1>message : {responseMessage}</h1>
		</div>
	);
}

 

//server.js

app.use((req, res, next) => {

	// Access-Control-Allow-Origin 헤더를 설정해주므로써, CORS 시나리오 만족!
	res.setHeader('Access-Control-Allow-Origin', '*');
	next();
});

const apiRouter = express.Router();
apiRouter.get('/simple', (req, res) => {
	console.log('hello');
	return res.send('hi from simple');
});

app.use('/', apiRouter);

Response

 

2. Preflighted Request

단순 요청과 달리, 요청을 전송하기에 안전한지 환경인지 확인하기 위해 사전 요청을 보냅니다. 일반적으로 , 커스텀 header을 사용하거나, UPDATE, DELETE메소드 사용 시, 시행 됩니다.

 

1. 본 요청 전에 OPTIONS 메서드로 사전 요청을 보냅니다.

2. 응답이 성공적일 경우, 본 요청을 보냅니다.

 

 

app.tsx에서 get -> delete으로 바꿔 요청을 보내보겠습니다.

 

preflight response & error

 

Preflight response header

과정 1처럼 사전 요청을 OPTION 방식으로 보낸것을 확인할 수 있는데, 본 요청에 있어서, 에러가 생겼습니다.

이는 서버에서 응답을 보낼 때, 다음과 같이 DELETE 메소드 허용 헤더를 붙여주는 방법으로 해결할 수 있습니다.

 

app.use((req, res, next) => {
	res.setHeader('Access-Control-Allow-Origin', '*');
    
        //DELETE method 만! 허용!
        //만약 모든 method 허용시키고 싶으면 와일드카드 '*'로 설정
        //복수의 method 허용시키고 싶다면 ','로 구분시켜서 쓰면됩니다 ex) 'POST,GET,DELETE'
	res.setHeader('Access-Control-Allow-Methods', 'DELETE'); 
	next();
});

 

그러면 다음과 같이 성공적인 응답을 얻을 수 있습니다. Response Header에 'Access-Control-Allow-Method' 헤더가 붙은게 보이네요

preflight response
main response

 

3. Credentialed Request

해당 요청은, 자격 증명 요소들을 인식하는데, 일반적으로 요청을 보낼 때, 브라우저는 자격 증명을 보내지 않기에, 추가적인 설정을 통하여 보낼 수 있습니다.

 

 

먼저 post 요청으로 바꾸고 , 정보를 담은 후, credential 요청을 보내겠습니다. 이전 preflight 요청의 다양한 옵션들을 체험할 겸, 커스텀 헤더도 넣어서 보내겠습니다.

 

const fetchToServer = async () => {
		const response: AxiosResponse<string> | void = await axios
			.post(
				SERVER + '/credential',
				{
					ids: 'dovi-id',
					passwords: 'dovi-password'
				},
				{
					withCredentials: true,
					headers: {
						'dovi-header': '0123456789',
						Authorization: 'mybearer bearer-token-exampleee'
					}
				}
			)
			.catch((e) => {
				console.log(e);
			});

 

예상했던대로, 에러가 뜹니다. 

Access-Control-Allow-Credentials 헤더와 커스텀 헤더에 대해 대응되는 헤더를 추가함으로써 해결할 수 있습니다.

//server.js

app.use((req, res, next) => {
	//credential request시, 와일드 카드 사용 x
	res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5173');
	//credential 요청 허용
	res.setHeader('Access-Control-Allow-Credentials', 'true');
	res.setHeader('Access-Control-Allow-Methods', '*');
	//body에 데이터 담으면 , Content-type , content-length header 필요합니다 , app.tsx에서 커스텀 헤더인 dovi-header을 넣었으므로, 해당 헤더도 허용해 줍시다
	res.header(
		'Access-Control-Allow-Headers',
		'Content-Type, Authorization, Content-Length, X-Requested-With, dovi-header'
	);
	next();
});

 

해결!