본문 바로가기
React-study/dil

[모던 리액트 Deep Dive] 14장 웹사이트 보안을 위한 리액트와 웹페이지 보안 이슈

by 어느새벽 2024. 12. 24.

 

14.1 리액트에서 발생하는 크로스 사이트 스크립팅(XSS)

 

웹사이트 개발자가 아닌 3 자가 웹사이트에 악성 스크립트를 삽입해 실행할 있는 취약점을 의미한다.

script 실행될 있다면 웹사이트 개발자가 있는 모든 작업을 함께 수행할 수 있으며, 쿠키를 획득해 사용자의 로그인 세션 등을 탈취하거나 사용자의 데이터를 변경하는 등 각위험성이 있다.

 

14.1.1 dangerouslySetlnnerHTMLprop

특정 브라우저 DOM innerHTML특정한 내용으로 교체할 있는 방법이다.

dangerouslySetlnnerHTML오직 __ html 키를 가지고 있는 객체인수받을 있으며, 인수로 겨받은 문자열을 DOM표시하는 역할을 한다. 그러나 dangerouslySetlnnerHTML 위험성은 dangerouslySetlnnerHTML 인수로 받는 문자열에는 제한이 없다는 것이다. 다음 예제보자.

const html = `<span><svg/onload=alert(origin)></span>`

function App() {
 return <div dangerouslySetInnerHTML = {{__html: html}} /> 
}

export default App

 

코드를 방문하면 다음과 같이 originalert 나타나게 된다.

그대로 dangerouslySetlnnerHTML사용에 주의를 기울여야 하는 prop 이며, 여기에 넘겨주는 문자열 은 한번 검증이 필요하다는것을 알수있다.

 

14.1.2 useRef를 활용한 직접 삽입

dangerouslySetlnnerHTML슷한 방법으로 DOM직접 내용을 삽입할 있는 방법으로 useRef있다.

useRef활용하면 직접 DOM 접근할 있으므로 DOM 앞서와 비슷한 방식으로 innerHTML 보안취약점이 있는 스크립트를 삽입하면 동일한 문제가 발생한다.

 

<script> svg/onload 를 사용하는 방식 외에도 <a> 잘못된 href 를 삽입하거나 onclick, onload 이벤트를 활용하는 가지 방식xss있지만 공통적사이트 개발자만들지 드를 삽입한다는 다.

 

14.1.3 리액트에서 XSS 문제를 피하는 방법

3자가 있는 HTMLHTML 코치환하는 이다. 

이러한 과정을 새니타이(sanitize) 케이프(escape) 라고 데, 새니타이즈직접 구현해 사용하는 다양한 방법이 만 가확실방법은 npm 있는 라브러리용하는 것이다.

이와 관된 유명한 라이브러리로는 다음과 같은 것다.

  • DOMpurity( https://github.com/cure53/DOMPurify)
  • sanitize-html(https://github.com/apostrophecms/sanitize-html)
  • js-xss( https://github.com/leizongmin/js-xss)

sanitize-html허용할 태그와 일일히 나열하는 이른바 허용 목록 (allow list) 방식이다.
이러한 치환 과정은 되도록 서버에서 수행하는 것이 좋다. 서버는 '클라이언트에서 사용자가 입력한 데이터는 일단 의심한다'라는 자세로 클라이언트의 POST 요청에 있는 HTML을 이스케이프하는 것이 제일 안전하다.

 

쿼리스트링에 있는 내용을 그대로 실행하거나 보여주는 경우에도 보안 취약점이 발생할 수 있다.

따라서 개발자는 자신이 작성한 코드가 아닌 query, GET 파라미터, 서버에 저장된 사용자가 입력한 데이터 등 외부에 존재하는 모든 코드를 위험한 코드로 간주하고 이를 적절하게 처리하는 것이 좋다.

 

리액트의 JSX 데이터 바인딩

dangerouslySetlnnerHTML이라는 속성이 별도로 존재하는 이유는 기본적으로 리액트가 XSS를 방어하기 위해 이스케이프 작업을 하기 때문이다.

따로 이스케이프 작업을 하지 않아도 실제로 실행되지 않는다. 즉, <div>{html}</div>와 같이 HTML에 직접 표시되는 textContent와 HTML 속성 값에 대해서는 리액트가 기본적으로 이스케이프 작업을 해주는 것을 알 수 있다.

그러나 dangerouslySetlnnerHTML이나 props로 넘겨받는 값의 경우 개발자의 활용도에 따라 원본 값이 필요할 수 있기 때문에 이러한 작업이 수행되지 않는다.

 

14.2 getServerSideProps와 서버 컴포넌트를 주의하자

서버에는 일반 시용자에게 되면 되는 정보들이 담겨 있기 때문에 클라이언트, 즉 브라우저에 정보를 내려줄때는 조심해야 한다.

 

getServerSideProps 에서 cookie 정보를 가져온 다음 이를 클라이언트 리액트 컴포넌트에 자열로 제공해 클라이언트에서 쿠키의 유효성에 따라 이후 작업을 처리한다. 이 코드가 보안이 안 좋은 이유는 getServerSideProps 반환하는 props 값은 모두 사용자의 HTML기록되고,  또한 전역 변수로 등록되스크립트로 충분히 접근할 있는 보안 위협에 노출되는 값이 되기 때문이다.

충분히 getServerSideProps에서 처리할 있는 리다이렉트가 클라이언트에서 실행되어 성능 측면에서도 손해를 본다. 따라서 getServerSideProps 반환하는 값 또는 서버 컴포넌트가 클라이언트 컴포넌트에 반환히는 props반드시 필요한 값으로만 철저하게 제한되어야 한다. 이는 보안 측면의 이점뿐만 아니라 성능 측면에서도 이점을 져다줄수 있다. 다음과 같이 수정하면,

 

쿠키 전체제공하는 것이 아니라 클라이언트에서 필요token 값만 제한적으로 반환했고, 이 값이 없을 예외 처리할 리다이렉트도 모두 서버에서 처리했다. 이로써 불필요하쿠키 값을 노출하는 것을 없앴고,리다이렉트 또한 한층 빨라질 것이다.

 

이러한 방식의 접근법은 비단 getServerSideProps 서버 컴포넌트뿐만 아니라 리덕스에서 서버 사이드에가져온 상태로 가져오는 window. __ PRELOADED_STATE 같은 값을 데이터 초기화할 때도 적용된다. window. PRELOADED_STATE __ 값은 XSS 취약 할 수 있기 때문에 반드시 새니타이즈를 거치고, 필요한 값만 제공해야 한다.

 

14.3 <a> 태그의 값에 적절한 제한을 둬야 한다

<a> 태그의 href javascript: 시작히는 자바스크립트 코드를 넣어둔 경우를 적이 있을 것이다. 이는 주로 <a> 태그의 기본 기능 href 선언된 URL페이지를 이동하는 것을 막고 onClick 벤트와 같이 별도 이벤트 핸들러만 작동시키기 위한 용도로 사용된.

하지만 <a> 태그는 반드시 페이지 이동이 있을 때만 사용하는 것이 좋다.

왜냐하면 href가 작동하지 않는 것이 아니고, href 내에 자바스크립트 코드가 존재한다면 이를 실행한다는 뜻이다. 따라서 코드를 실행하면경고문과 함께 정상적으로 렌더링되는 것을 확인 할 수 있다.

XSS에서 소개한 사례와 비슷하게, href 사용자가 입력한 주소를 넣을 있다면 또한 보안 이슈로 이어질 있다. 따라서 href 들어갈 있는 값을 제한해야 한다. 그리고 피싱 사이트로 이동하는 것을 막기 위해 가능하다면 origin 확인해 리하는 것이 좋다.

function isSafeHref(href:string) {
 let isSafe = false
 try {
	//javascript:가 오면 protocol이 javascript:가 된다.
 	const url = new URL(href)
	if(['http:', 'https:']).includes(url.protocol)){
     isSafe = true
    }
 } catch {
	isSafe = false
   }
 return isSafe
}

 

위에 함수를 만들어서 안전한 주소면 true, 위험한 주소면 false를 반환하여 처리한다.

 

14.4 HTTP 보안 헤더 설정하기

14.4.1 Strict-Transport-Security

HTTP의 Strict-Transport-Security 응답 헤더는 모든 사이트가 HTTPS를 통해 접근해야 하며, 만약 HTTP로 접근하는 경우 이러한 모든 시도는 HTTPS로 변경되게 한다. 사용법은 다음과 같다.

Strict-Transport-Security: max-age=<expire-time>; includeSubDomains

 

<expire-time>은 이 설정을 브라우저가 기억해야 하는 시간을 의미하며, 초 단위로 기록된다. 이 기간 내에 HTTP로 사용자가 요청해도 브라우저는 이 시간을 기억했다가 자동으로 HTTPS로 요청하게 된다. 이 시간이 경과하면 HTTP로 로드를 시도한 뒤 응답에 따라 HTTPS로 이동하는 등의 작업을 수행한다. 시간이 0으로 돼 있다면 헤더가 즉시 만료되고 HTTP로 요청하게 된다. 권장값은 2년이다.

 

14.4.2 X-XSS-Protection

X-XSS-Protection은 비표준 기술로, 현재 사파리와 구형 브라우저에서만 제공되는 기능이다.

헤더는 페이지에서 xss 취약점이 발견되면 페이지 로딩을 중단하는 헤더다. Content-Security-Policy지원하지 않는 구형

라우저에서 사용이 가능하다. 반드시 페이지 내부에 XSS에 대한 처리가 존재하는지 확인해야 한다.

 

 

14.4.3 X-Frame-Options

X-Frame-Options 페이지를 frame , iframe, embed , object 내부에서 렌더링을 허용할지를 나타낼 있다.

 

X-Frame-Options: DENY 제3의 페이지 내부에 사이트를 삽입하려고 하면 무조건 막는다.

X-Frame-Options: SAMEORIGIN 같은 origin에 대해서만 프레임을 허용한다.

 

14.4.4 Permissions-Policy

웹사이트에서 사용할 수 있는 기능과 사용할 수 없는 기능을 명시적으로 선언하는 헤더다.

예를 들어, 브라우저에서 사용자의 위치를 확인하는 기능(geolocation)과 관련된 코드를 작성하지 않고 별도로 차단하지 않는다면 XSS 공격 등으로 이 기능을 취득해 사용자의 위치를 획득하는 위험이 있다. 이 헤더를 활용하여 사용자에게 미칠 수 있는 악영향을 제한할 수 있다.

제어할 수 있는 목록은 MDN 문서 또는 https://www.permissionspolicy.com/에서 확인할 수 있다.


 

14.4.5 X-Content-Type-Options

먼저 MIME에 대한 이해가 필요한데, MIME란 Multipurpose Internet Mail Extenstions의 약자로 Content-type의 값으로 사용된다. 

원래는 메일을 전송할 때 사용하던 인코딩 방식으로, 현재는 Content-type에서 대표적으로 사용되고 있다.

 

네이버에서는 www.naver.com Content-Type: text/html; charset=UTF-8 반환해 브라우저가 이를 UTF-8인코딩된 text/html 인식할 있게 도와주고브라우저는 헤더를 참고해 해당 파일에 대해 HTML을 파싱하는 과정을 거치게 된다. 이러한 MIMEjpg, CSS, JSON 등 다양하다.

X-Content-Type-Options Content-type 헤더에서 제공하는 MIME 유형이 브라우저에 의해 임의로 변경되지 않게 하는 헤더다. 

 

예를들어, 공격자가 .jpg 파일을 웹서버에 업로드 했는데 실제로 그림 관련 정보가 아닌 악의적인 스크립트가 담겨 있음에도 해당 코드를 실행하는 위험이 있다. 이런 경우 다음과 같은 헤더를 설정해 두면 파일의 타입이 css MIME text/css 아닌 경우, 혹은 파일

용이 script나 MIME 타입이 자바스크립트 타입이 아니면 차단하게 된다.

X-Content-Type-Options: nosniff

 

14.4.6 Referrer-Policy

HTTP 요청에는 Referer 라는 헤더가 존재하는데, 헤더에는 현재 요청을 보낸 페이지의 주소가 나타난다. 헤더는 사용자가 어디서 와서 방문중인지 인식 할 수 있헤더지만, 반대로 사용자 입장에서는 원치 않는 정보가 노출될 위험도 존재한다. 

Referrer-Policy 헤더는 Referer 헤더에서 사용할 있는 데이터를 나타낸다.

 

Referer 대해 이야기할 때는 출처(origin)빼놓을 없다.

먼저 https://yceffort.kr 이라는 주소의 출처는 다음과 같이 구성돼 있다.

  • scheme: HTTPS 프로토콜을 의미한다.
  • hostname: yceffort. kr 이라는 호스트명을 의미한다.
  • port: 443 포트를 의미한다.

위 세 가지 조합을 출처라고 한다.

그리고 두 주소를 비교할때 same-origin 인지, cross-origin 인지는 다음과 같이 구분할 수 있다. 

yceffort.kr:443기준으로 비교했을 다음과 같이 나타낼 있다.

 

Referrer-Policy는 응답 헤더 뿐만 아니라 페이지의 <meta/> 태그로도 다음과 같이 설정할 수 있다.

<meta name="referrer" content="origin" />

 

페이지 이동 시나 이미지 요청, link 태그 등에도 다음과 같이 사용할 수 있다.

<a href="http://yceffort.kr" referrerpolicy="origin">...<a/>

 

구글에서는 이용자의 개인정보 보호를 strict-origin-when-cross-origin 혹은 그 이상을 명시적으로 선언해 둘 것을 권고한다.  


더보기

meta태그 안에 설정할 때는 index.html 파일의 head 태그 내부에 추가하면 된다.

 

14.4.7 Content-Security-Policy

콘텐츠 보안 정책 (Content-Security-Policy, 이하 CSP)XSS 공격이나 데이터 삽입 공격과 같은 다양한 보안 위협을 막기 위해 설계됐다. 지시문이 굉장히 많으며, 다음은 대표적으로 이용되는 지시문이다. 모든 지시문은 웹 표준을 정의한 W3에서 확인 가능하다.

 

*-src

font-src , img-src, script-src 다양한 src 제어할 있는 지시문이다. 

Content-Security-Policy: font-src < source>;

Content-Security-Policy: font-src < source> < source>;

 

위처럼 선언해 두면 font 소스만 가져올 수 있고 외에는 모두 차단된다.

 

비슷한 유형의 지시문은 다음과 같다.

 

만약 해당 -src가 선언돼 있지 않다면 default-src로 한번에 처리할 수도 있다.

Content-Security-Policy: default-src < source>;
Content-Security-Policy: default-src < source> < source>;

 

form-action

form 양식으로 제출할 수 있는 URL을 제한할 수 있다. 다음과 같이 form-action 자체를 모두 막아버리는 것도 가능하다.

 

위에 submit을 눌러 form을 제출하면 콘솔에 다음과 같은 에러메시지와 함께 작동하지 않는다.

 

14.4.8 보안헤더 설정하기

 

Next.js

Next.js에서는 애플리케이션 보안을 위해 HTTP 경로별로 보안 헤더를 적용할 수 있다. next.config.js에서 다음과 같이 추가할 수 있다.

const securityHeaders = [
 {
	key: 'key',
	value: 'value',
 },
]

module.exports = {
 async headers() {
	return [
		{
          //모든 주소에 설정한다.
          source: '/:path*',
          headers: securityHeaders,
		},
	]
 },
}

 

여기서 설정할 수 있는 값은 다음과 같다.

 

NGINX

정적인 파일을 제공하는 NGINX의 경우 다음과 같이 경로별로 add_header 지시자를 사용해 원하는 응답  헤더를 추가할 수 있다.

 

14.4.9 보안 헤더 확인하기

현재 웹사이트의 보안 헤더를 확인할 수 있는 가장 빠른 방법은 보안 헤더의 현황을 알려주는 https://securityheaders.com/을 방문하는 것이다. 헤더를 확인하고 싶은 웹사이트의 주소를 입력하면 보안 헤더 상황을 알 수 있다.

 

14.5 취약점이 있는 패키지의 사용을 피하자

패키지들은 버전에 따라 보안 취약점이 존재할 수도 혹은 업데이트 이후에 보안 취약점이 드러나거나 파악되지 않았던 취약점이 나타날 수도 있다. 따라서 깃허브의 Dependabot발견한 취약필요하다면 업데이트해 조치해야 한다.

https://security.snyk.io/방문해 사용 중인 이름으로 검색해 보면 현재 라이브러리의 취약점을 한눈에 파악할 있다.

 

14.6 OWASP Top 10

OWASP Open Worldwide (Web) Application Security Project 라는 오픈소스 리케이션 보안 프로젝트의미한다. 

주로 웹에서 발생할 있는 정보 악성 스크립트보안 취약점 등을 연구하며, 기적으10애플리케이션 취약점공개하는데 이를 OWASP Top 10 이라고 한다.

보안 취약점을 요약해 주는 것뿐만 아니라 이 문제를 어떻게 조치해야 하는지도 자세히 소개한다.

 

 


 

현재 개발 중인 개인프로젝트로 테스트 해봤다. 

https://securityheaders.com/에 들어가서 사이트 주소 입력하니 나온 처참한 결과 ㅋㅋㅋㅋㅋㅋ

Strict-Transport-Security는 supabase를 써서 자동으로 적용이된 것 같았다.

보안 설정 전

헤더 옵션은 서버에서 설정해야 하는데 React는 서버가 없어서 사용하고 있는 배포 프로그램에 넣어주면 된다고 한다. 나는 vercel로 배포해서 다음과 같이 vercel 설정 파일을 만들어 넣어줬다.

 

//index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/icon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="referrer" content="strict-origin" />
    <title>My Library</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

 

위에 말한 것처럼 referrer-policy를 meta 태그에 설정하는 방법으로 head태그 안에 추가하면 된다.

그런데 이렇게 html에 추가만 하니 적용이 안됐다.

 

 

아래 vercel.json에 Referrer-Policy: strict-origin 헤더를 추가해야만 적용되는거였다 ! 

//vercel.json

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Referrer-Policy",
          "value": "strict-origin"
        },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        {
          "key": "Content-Security-Policy",
          "value": "font-src 'none'"
        },
        {
          "key": "Permissions-Policy",
          "value": "camera=(), microphone=(), geolocation=(), fullscreen=(self)"
        }
      ]
    }
  ]
}

 

 

 

마지막 X-Frame-Options는 iframe이나 그런걸 안 써서 안 했는데 해야 되나용?