본문 바로가기

Study

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

웹사이트의 성능만큼이나 중요한 것 → 웹사이트의 보안

프론트엔드에서 해야 할 일이 많아질수록 프론트엔드의 보안 위험성이 증대되고 있다.

그렇다면, 프론트엔드 개발자가 신경 써야 할 다양한 보인 이슈는 무엇이 있을까?

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


크로스 사이트 스크립팅(Cross-Site Scription, XSS)란 웹사이트 개발자가 아닌 제 3자가 웹사이트에 악성 스크립트를 삽입해 실행 할 수 있는 취약점을 의미한다.

<p>사용자가 글을 작성했습니다.</p>
<script>
	alert('XSS')
</script>

예를 들어 사용자가 위처럼 게시글을 작성했다면, 위 글 방문 시 script도 실행되어 window.alert도 함께 실행된다.

이렇게 script에 대한 별도의 조치가 없어 실행된다면 웹사이트 개발자가 할 수 있는 모든 작업을 함께 수행할 수 있는 문제가 발생하게 된다.

이 XSS 이슈는 리액트에서 어떻게 발생 할 수 있을까?

1. dangerouslySetInnerHTML prop

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

function App() {
	// 다음 결과물은 <div>First . Second</div> 이다.
	return <div dangerouslySetInnerHTML={{ __html: 'First &middot; Second' }} />
}

그러나 dangerouslySetInnerHTML의 위험성은 인수로 받는 문자열에는 제한이 없다는 것이다.

따라서 사용에 주의를 기울여야 하며 인수로 넘겨주는 문자열은 한번 더 검증이 필요하다.

2. useRef를 활용한 직접 삽입

dangerouslySetInnerHTML와 비슷한 방법으로 DOM에 직접 내용을 삽입 할 수 있는 방법이다. 앞 예제와 비슷한 방식으로 innerHTML에 보안 취약점이 있는 스크립트를 삽입하면 동일한 문제가 발생한다.

이 외에도 여러가지 방식의 XSS가 있지만 공통적인 문제는 웹사이트 개발자가 만들지 않은 코드를 삽입한다는 것에 있다.

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

리액트에서 XSS 이슈를 피하는 가장 확실한 방법?

⇒ 제 3자가 삽입할 수 있는 HTML을 안전한 HTML 코드로 치환하는 것 (새니타이저 or 이스케이프)

이와 관련된 유명한 라이브러리

  • DOMpurity
  • sanitize-html
  • js-xss

sanitize-html을 사용하면 허용할 태그와 속성을 정의 할 수 있다. 이렇게 허용 목록을 나열하는 것이 귀찮을 수 있지만, 허용 목록을 작성하는 것이 차단 목록을 작성하는 것보다 훨씬 안전하다.

허용 목록에 추가하는 것을 깜박한 태그나 속성이 있다면 단순 HTML이 안보이는 사소한 이슈로 그치겠지만 차단 목록으로 해야 할 것을 놓친다면 그 즉시 보안이슈로 연결되기 때문이다.

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


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

export function App({cookie}: {cookie: string}) {
    if(!validateCookie(cookie)) {
        Router.replace()
        return null
    }
}

export const getServerSideProps = async(ctx: GetServerSidePropsContext) => {
    const cookie = ctx.req.headers.cookie || ''
    
    return {
        props: {
            cookie
        }
    }
}

위 예제는 서버에서 쿠키 정보를 가져와 클라이언트에서 쿠키에 유효성에 따라 작업을 처리하는 코드이다. 이 코드는 보안 관점에서 썩 좋지 못하다.

  1. getServerSideProps가 반환하는 props 값은 모두 사용자의 HTML에 기록된다.
  2. 전역 변수로 등록되어 스크립트로 충분히 접근 할 수 있어 보안 위협에 노출되는 값이 된다.
  3. 리다이렉트가 클라이언트에서 실행되어 성능 측면에서도 손해를 본다.

따라서, getServerSideProps가 반환하는 값 또는 서버 컴포넌트가 클라이언트 컴포넌트에 반환하는 props는 반드시 필요한 값으로만 철저하게 제한되어야 한다.

export function App({token}: {token: string}) {
    const user = JSON.parse(window.atob(token.split('.')[1]))
    const user_id = user.id
}

export const getServerSideProps = async(ctx: GetServerSidePropsContext) => {
    const cookie = ctx.req.headers.cookie || ''

    const token = validateCookie(cookie)

    if(!token) {
        return {
            redirect: {
                destination: '/404',
                permanent: false
                }
            }
    }

    return {
        props: {
            cookie
        }
    }
}

앞에 예제 코드를 수정하면 위와 같다.

쿠키 전체를 제공하는 것이 아닌 필요한 token 값만 제한적으로 반환했고, 값이 없을 경우 예외처리 할 리다이렉트도 모두 서버에서 처리했다.

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


export function App() {
    function handleClick() {
        console.log('hello')
    }
    
    return (
        <>
            <a href="javascript:" onClick={handleClick}></a>
        </>
    )
}

위 처럼 <a> 태그의 href에 javascript: 로 시작하는 자바스크립트 코드를 넣어두면, href로 선언된 URL로 페이지가 이동되는 것을 막고 onClick 이벤트와 같이 별도 이벤트 핸들러만 작동시킨다.

위 방식은 마크업 관점에서 안티패턴이며 <a> 태그는 반드시 페이지 이동이 있을 때만 사용하는 것이 좋다.

href에 javascript 코드가 들어 갈 수 있다는 의미로 이전에 XSS에서 소개한 사례와 비슷한 보안 이슈가 나타 날 수 있다.

HTTP 보안 헤더 설정하기


HTTP 보안 헤더란 브라우저가 렌더링하는 내용과 관련된 보안 취약점을 미연에 방지하기 위해 브라우저와 함께 작동하는 헤더를 의미한다. 대표적인 HTTP 보안 헤더에는 무엇이 있을까?

Strict-Transport-Security

모든 사이트가 HTTPS를 통해 접근해야 하며, 만약 HTTP로 접근하는 경우 이러한 모든 시도는 HTTPS로 변경되게 하는 헤더이다.

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

<expire-time> : 브라우저가 기억해야 하는 시간

includeSubDomains가 있을 경우 이 규칙이 모든 하위 도메인에도 적용된다.

X-XSS-Protection

XSS 취약점이 발견되면 페이지 로딩을 중단하는 헤더이다. Content-Security-Policy가 있다면 그다지 필요 없지만 이 헤더를 지원하지 않는 구형 브라우저에서는 사용 가능하다.

X-Frame-Options

frame, iframe, embed, object 내부에서 렌더링을 허용 할 지를 나타 낼 수 있다. 외부에서 자신의 페이지를 위와 같은 방식으로 삽입되는 것을 막아주는 헤더이다.

Permissions-Policy

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

X-Content-Type-Options

Content-type 헤더에서 제공하는 MIME 유형이 브라우저에 의해 임의로 변경되지 않게 하는 헤더이다. 즉, Content-type: text/css 헤더가 없는 파일은 브라우저가 임의로 CSS로 사용 할 수 없으며, Content-type: text/javascript나 Content-type: application/javascript 헤더가 없는 파일은 자바스크립트로 해석 할 수 없다.

Referrer-Policy

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

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

Content-Security-Policy

Content-Security-Policy은 XSS 공격이나 데이터 삽입 공격과 같은 다양한 보안 위협을 막기 위해 설계 됐다. 다양한 지시문을 통해 코드에 제한을 걸어 보안 위협을 막을 수 있다.

Next.js 보안 헤더 설정하기

next.config.js에 다음과 같이 추가해 보안 헤더를 설정할 수 있다.

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

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

경로별로 보안 헤더를 적용 할 수 있으며, 여기서 설정 할 수 있는 값은 다음과 같다.

  • X-DNS-Prefetch-Control
  • Strict-Transport-Security
  • X-XSS-Protection
  • X-Frame-Options
  • Permissions-Policy
  • X-Content-Type-Options
  • Referrer-Policy
  • Content-Security-Policy

OWASP Top 10

OWASP는 Open Worldwide (Web) Application Security Project라는 오픈소스 웹 애플리케이션 보안 프로젝트를 의미한다. 정보 노출, 악성 스크립트, 보안 취약점 등을 연구하며 주기적으로 10대 웹 애플리케이션 취약점을 공개한다.