FrontEnd Roadmap 05 - 인터넷 파트5: Web Security 2: CORS 및 CSP

SANGHYUN KIM·2025년 1월 16일
0

frontend-roadmap

목록 보기
5/9

A. Cross-Origin Resource Sharing(CORS)

브라우저는 same-origin policy를 기반으로 작동한다.
그럼 same-origin policy부터 확인해본다.

A-1. Same Origin Policy(SOP)

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)되는 것들은 허용

iframesX-Frame-Options에 따라 변동되지만 대부분 허용. 그러나 cross-origin reading(JS를 활용하여 접급)은 불가
CSS<link> 또는 import를 통해서 CSS 파일 적용 가능. 단, 정확한 Content-Type 필요
formsCross-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.
imagesembedding은 가능하지만 읽고 쓰는 것(JS 활용)은 불가능
multimedia<video> 및 <audio>를 통해서 embed 가능
script교차 출처 embed 가능. 그러나 몇몇 API(예: 교차출처 fetch 요청) 접근 불가능

내장되는 리소스를 제외하더라도 다른 리소스와의 통신이 필요했기에, 필요에 의해 CORS 탄생.(Fetch Standard - 3.2 CORS protocol)

A-2. CORS 작동 방식

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

  1. 다음 항목을 모두 만족 시 simple request로 간주
    1. GET, HEAD, POST 중 하나인 경우
    2. 자동을 설정 되는 헤더값 + Accept, Accept-Language, Content-Language으로 한정되는 경우
    3. Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/pain으로 한정되는 경우
    4. 요청에 ReadableStream가 사용되지 않는 경우
  2. 브라우저가 자동으로 Origin값을 부착하여 요청

img

  1. 서버는 요청을 받고 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 Request

  1. 다음 항목을 만족할 시 Preflight 요청

    1. GET, POST, HEAD가 아닌 다른 method
    2. Accept, Accept-Language, Content-Language 이외의 헤더를 포함할 경우
    3. application/x-www-form-urlencoded, multipart/form-data, text/pain 이외의 Content-Type일 경우
  2. 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는 헤더 요청이 갈 것이라는 것을 표현
  3. 서버는 적합하다면 다음과 같이 응답 반환

    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 요청은 다음 초까지 요청하지 않는 것을 표기
  4. 이후 POST 요청 통신

A-2-3. Request with credentials

Request with credentials

  1. fetch에는 credentials: “include” 또는 XMLHttpRequest에는 withCredentials: true을 설정하여 요청

  2. [preflight만 적용] preflight 요청을 보낼 때 인증정보는 절대 포함되서는 안되고 응답값에는 Access-Control-Allow-Credenitals: true값이 포함되어야 함

  3. 각 통신 함수들이 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
  4. 인증정보가 맞다면 아래와 같이 응답

    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로 처리
    • 서버는 절대로 다음 4가지를 *wildcard 설정 하지 말고 되도록이 특정값들로 구성되기 설정 필요
      • Access-Control-Allow-Origin
      • Access-Control-Allow-Headers
      • Access-Control-Allow-Methods
      • Access-Control-Expose-Headers

B. Content Security Policy(CSP)

web.dev의 Content security policy를 기반으로 작성되었습니다.

위에서 브라우저는 SOP기반으로 작동한다고 했다. 그러나 이를 위반하여 공격을 하는 행위가 있고 이를 Cross-site scripting(XSS)라고 한다.
CSP는 XSS를 줄이기 위한 방법이다.

web.dev에서 효과적은 CPS 설정을 위해서 다음과 같이 권유:

  • Source allowlists을 사용하여 클라이언트에게 허용 가능한 범위 제한
  • 사용가능한 directives 탐색
  • 키워드 공부(Learn the keywords they take)
  • 인라인 코드와 eval()의 사용을 제한
  • 정책 위반 사항을 시행하기 전에 서버에 보고

B-1. Source allowlists + Directives + keywords

아래와 같이 출처를 제한할 수 있으며 다음과 같이 해석이 된다.

Content-Security-Policy: script-src 'self' https://apis.google.com

  • script source 컨트롤 지시문(directive)
  • HTML문서와 같은 곳의 source + 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 사용자라면 다음과 같이 설정 가능하다.

  • index.html 내부에 meta 태그로 설정
  • nginx 내부에서 응답 header에 설정

MPA 사용자라면 다음과 같이 설정 가능하다(참고 블로그)

  • page.(js | jsx | tsx) or layout.(js | jsx | tsx)
    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:;",
      },
    };
  • nextConfig 설정
    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:;",
              },
            ],
          },
        ];
      },
    };

B-2. eval() 사용하지 않기

먼저 eval은 어떤 함수인가?
MDN에 따르면 “문자로 표현된 JavaScript 코드를 실행하는 함수”라고 한다.

console.log(eval('2 + 2'));
// Expected output: 4

console.log(eval('2 + 2') === eval('4'));
// Expected output: true

이 함수를 사용하면 임의의 함수를 실행할 수 있게 되고 공격자가 전달한 함수를 실행하게 될 수 있다. 따라서, eval()사용을 지양하지만 기능이 우선시 된다면 unsafe-eval을 통해서 허용할 수 있다.

Q. 근데 eval이 어디에 사용되었던 것일까?

web.dev에는 아래와 같이 기재되어있다.

”You must parse JSON using the built-in JSON.parse, instead of relying on eval. 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

B-3. 정책 위반 신고

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;

C. SOP와 CSP

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

Fetch Standard

Demystifying CORS: Understanding How Cross-Origin Resource Sharing Works

CSP

Content security policy  |  Articles  |  web.dev

eval() - JavaScript | MDN

eval()과 CSP

JavaScript .eval() 함수란? (feat. 쓰면 안되는 이유)

Next에서 CSP 설정 방법 2가지

profile
꾸준히 공부하자

0개의 댓글