브라우저는 same-origin policy를 기반으로 작동한다.
그럼 same-origin policy부터 확인해본다.
web.dev의 Same Origin Policy (SOP)를 기반으로 작성되었습니다.
동일한 출처의 리소스만을 허용하는 브라우저의 보안 정책이다.
이 때 출처는 무엇일까?
출처(Origin) = 스킴(프로토콜, HTTP or HTTPS) + 호스트 + 포트
위 3개가 동일할 때 동일한 출처라고 간주한다.
원본 http://www.example.com/foo
통신대상1: http://www.example.com/foo // 동일출처
통신대상2: https://www.example.com/foo // 동일출처 X
원본 http://www.example.com/foo:80
통신대상1: http://www.example.com/foo:80 // 동일출처
통신대상2: https://www.example.com/foo:100 // 동일출처 X
기본적으로 브라우저는 다른 출처를 막지만, html 문서에 내장(embedded)되는 것들은 허용
| iframes | X-Frame-Options에 따라 변동되지만 대부분 허용. 그러나 cross-origin reading(JS를 활용하여 접급)은 불가 |
| CSS | <link> 또는 import를 통해서 CSS 파일 적용 가능. 단, 정확한 Content-Type 필요 |
| forms | Cross-origin URLs can be used as the action attribute value of form elements. A web application can write form data to a cross-origin destination. |
| images | embedding은 가능하지만 읽고 쓰는 것(JS 활용)은 불가능 |
| multimedia | <video> 및 <audio>를 통해서 embed 가능 |
| script | 교차 출처 embed 가능. 그러나 몇몇 API(예: 교차출처 fetch 요청) 접근 불가능 |
내장되는 리소스를 제외하더라도 다른 리소스와의 통신이 필요했기에, 필요에 의해 CORS 탄생.(Fetch Standard - 3.2 CORS protocol)
mdn의 Cross-Origin Resource Sharing (CORS)를 기반으로 작성되었습니다.
CORS 요청은 아래 두개로 나눌 수 있다
A-2-1. Simple Request(단순요청)
아래 MDN 원문에 따르면 Simple Request 용어는 최신 spec(Fetch spec에 CORS 정의가 통합되고 기존 CORS는 outdated 처리)서는 사용되지 않지만 구분을 위해서 사용 가능.
Some requests don't trigger a CORS preflight. Those are called simple requests from the obsolete CORS spec, though the Fetch spec (which now defines CORS) doesn't use that term. - MDN: CORS - Simple request
- 다음 항목을 모두 만족 시 simple request로 간주
- GET, HEAD, POST 중 하나인 경우
- 자동을 설정 되는 헤더값 + Accept, Accept-Language, Content-Language으로 한정되는 경우
- Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/pain으로 한정되는 경우
- 요청에 ReadableStream가 사용되지 않는 경우
- 브라우저가 자동으로 Origin값을 부착하여 요청
서버는 요청을 받고 CORS header값을 추가한 응답(response) 전달
app.get('/data', (req, res) => {
const allowedOrigins = ['http://localhost:4000', 'https://otherdomain.com'];
const requestOrigin = req.headers.origin;
if (allowedOrigins.includes(requestOrigin)) {
res.header('Access-Control-Allow-Origin', requestOrigin);
res.header('Access-Control-Allow-Methods', 'GET');
res.status(200).json({ data: 'Response data' });
} else {
res.status(403).json({ error: 'Unauthorized origin' });
}
});
Access-Control-Allow-Origin 값이 *라면 출처에 관계없이 액세스 가능Access-Control-Allow-Origin 값이 특정 origin을 반환하면 해당 도메인만 접근 가능A-2-2. Preflight Request
다음 항목을 만족할 시 Preflight 요청
OPTIONS method 요청 진행
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother
Access-Control-Request-Method: POST는 POST 요청이 갈 것이고Access-Control-Request-Headers: content-type,x-pingother는 헤더 요청이 갈 것이라는 것을 표현서버는 적합하다면 다음과 같이 응답 반환
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Access-Control-Allow*로 시작하는 모든 값은 해당 value의 값을 허용한다는 의미Access-Control-Max-Age: 86400는 preflight 요청은 다음 초까지 요청하지 않는 것을 표기이후 POST 요청 통신
A-2-3. Request with credentials
fetch에는 credentials: “include” 또는 XMLHttpRequest에는 withCredentials: true을 설정하여 요청
[preflight만 적용] preflight 요청을 보낼 때 인증정보는 절대 포함되서는 안되고 응답값에는 Access-Control-Allow-Credenitals: true값이 포함되어야 함
각 통신 함수들이 cookie를 설정하여 인증정보를 포함하여 통신
GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: https://foo.example/examples/credential.html
Origin: https://foo.example
Cookie: pageAccess=2
인증정보가 맞다면 아래와 같이 응답
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
Access-Control-Allow-Credenitals값이 true이 아닌 경우Access-Control-Allow-Origin: * 가 포함되는 경우 CORS로 처리*wildcard 설정 하지 말고 되도록이 특정값들로 구성되기 설정 필요web.dev의 Content security policy를 기반으로 작성되었습니다.
위에서 브라우저는 SOP기반으로 작동한다고 했다. 그러나 이를 위반하여 공격을 하는 행위가 있고 이를 Cross-site scripting(XSS)라고 한다.
CSP는 XSS를 줄이기 위한 방법이다.
web.dev에서 효과적은 CPS 설정을 위해서 다음과 같이 권유:
아래와 같이 출처를 제한할 수 있으며 다음과 같이 해석이 된다.
Content-Security-Policy: script-src 'self' https://apis.google.com
Directive 종류는 다양하고 default-src 설정한다면 -src로 끝나는 script-src, font-src, img-src 등등에 제한을 걸 수 있다
그리고 기본적으로 브라우저는 특정 지시문이 없다면 제한 없이 모든 출처 리소스를 로드하고 사용하기에 필요한 것은 설정하라고 권장
이런 설정은 HTTP header에서 설정이 선호되지만, page(html)내부 meta 태그를 통해서도 설정이 가능
<meta http-equiv="Content-Security-Policy" content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'">
Q. 그럼 (React 기반)SPA나 MPA는 어떻게?
SPA 사용자라면 다음과 같이 설정 가능하다.
MPA 사용자라면 다음과 같이 설정 가능하다(참고 블로그)
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'LoginPage',
description: '로그인 페이지',
other: {
'http-equiv': 'Content-Security-Policy',
content:
"default-src 'self' 'unsafe-eval' 'unsafe-inline' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:;",
},
};const nextConfig = {
async headers() {
return [
{
source: '/login',
headers: [
{
key: 'Content-Security-Policy',
value:
"default-src 'self' 'unsafe-eval' 'unsafe-inline' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:;",
},
],
},
];
},
};먼저 eval은 어떤 함수인가?
MDN에 따르면 “문자로 표현된 JavaScript 코드를 실행하는 함수”라고 한다.
console.log(eval('2 + 2'));
// Expected output: 4
console.log(eval('2 + 2') === eval('4'));
// Expected output: true
이 함수를 사용하면 임의의 함수를 실행할 수 있게 되고 공격자가 전달한 함수를 실행하게 될 수 있다. 따라서, eval()사용을 지양하지만 기능이 우선시 된다면 unsafe-eval을 통해서 허용할 수 있다.
web.dev에는 아래와 같이 기재되어있다.
”You must parse JSON using the built-in
JSON.parse, instead of relying oneval. Safe JSON operations are available in every browser since IE8.”
또한, eval에 대해서 한 블로그 답글에 이렇게 쓰여져 있다.
”옛날에 json같은것들 받을때 쓰이더라고요 보안성때문에 안쓰지만”
이 출처에서 eval()의 사용 예를 확인해볼 수 있었고 web.dev의 JSON.parse 권유도 이해할 수 있었다.
// https://play-with.tistory.com/299
// JSON 포맷이 도입되기 전에는 서버에서 받은 데이터를 객체로 변환하기 위해 `eval()`을 사용하기도 했습니다:
let jsonData = '{"name": "John", "age": 30}';
let obj = eval('(' + jsonData + ')');
console.log(obj.name); // John
CSP 정책이 위반될 때마다 원하는 서버로 정책위반 내용을 보내주는 기능이 브라우저에 있다.
report-uri속성을 활용하면 뒤에 있는 서버 주소로 브라우저가 다음과 같은 내용을 포함하여 자동적으로 보내준다
{
"csp-report": {
// CSP 위반이 발생한 페이지
"document-uri": "http://example.org/page.html",
// 페이지 요청한 사용자의 참조 URL
"referrer": "http://evil.example.com/",
// 차단된 리소스의 URL
"blocked-uri": "http://evil.example.com/evil.js",
// 위반된 CSP 정책
"violated-directive": "script-src 'self' https://apis.google.com",
// 적용된 전체 CSP 정책
"original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
}
}
점진적으로 CSP 적용하기(report-only)
CSP가 적용되어서 바로 차단되는 상황을 방지하거나 CSP를 점진적으로 적용시키는 단계로 나아가기 위해 Content-Security-Policy대신 Content-Secruity-Policy-Report-Only속성 적용 가능
Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;
네이버 로그인 HTML의 응답 헤더를 보면 다음과 같이 되어 있는 것을 실제로 확인 가능
// https://nid.naver.com/nidlogin.login?mode=form&url=https://www.naver.com/
Content-Security-Policy: script-src 'nonce-Iv2SGMkN3avdl208Hxrk9gAL' *.nid.naver.com ntm.pstatic.net ssl.pstatic.net wtm.pstatic.net ncpt.naver.com 'wasm-unsafe-eval' 'unsafe-inline' 'self'; img-src s.pstatic.net tivan.naver.com 'self' data: ssl.pstatic.net phinf.pstatic.net ndevthumb-phinf.pstatic.net sp.naver.com static.nid.naver.com lcs.naver.com cc.naver.com soundcaptcha.nid.naver.com captcha.nid.naver.com; media-src 'self' soundcaptcha.nid.naver.com captcha.nid.naver.com; object-src 'self' soundcaptcha.nid.naver.com 'unsafe-inline'; report-uri https://nid.naver.com/login/api/csp.repo.naver;
Content-Security-Policy-Report-Only: script-src 'nonce-Iv2SGMkN3avdl208Hxrk9gAL' *.nid.naver.com ntm.pstatic.net ssl.pstatic.net wtm.pstatic.net ncpt.naver.com 'wasm-unsafe-eval' 'self'; img-src s.pstatic.net tivan.naver.com 'self' data: ssl.pstatic.net phinf.pstatic.net ndevthumb-phinf.pstatic.net sp.naver.com static.nid.naver.com lcs.naver.com cc.naver.com soundcaptcha.nid.naver.com captcha.nid.naver.com; media-src 'self' soundcaptcha.nid.naver.com captcha.nid.naver.com; object-src 'self' soundcaptcha.nid.naver.com ; report-uri https://nid.naver.com/login/api/csp.repo.naver.only;
CSP문서를 보다가 “By default, the browser loads the associated resource from any origin”라는 것을 보고 SOP 정책과 상반되는 내용이라고 생각.
그래서 둘의 관계를 궁금해서 GPT에게 물어봤고 다음과 같이 답변.
SOP와 CSP의 관계
- SOP는 항상 활성화되며 출처(클라이언트와 서버) 간 데이터 접근을 제한하는 메커니즘.
- CSP는 SOP 위에 어떤 리소스가 로드될 수 있는지를 제어하는 메커니즘
- 두 정책은 서로 독립적으로 동작하며, CSP는 SOP의 기본 보안 위에 추가적인 제한을 설정하는 역할을 함
CORS
Same-origin policy | Articles | web.dev
Cross-Origin Resource Sharing (CORS) | Articles | web.dev
Demystifying CORS: Understanding How Cross-Origin Resource Sharing Works
CSP
Content security policy | Articles | web.dev