웹사이트의 성능만큼이나 중요한 것 → 웹사이트의 보안
프론트엔드에서 해야 할 일이 많아질수록 프론트엔드의 보안 위험성이 증대되고 있다.
그렇다면, 프론트엔드 개발자가 신경 써야 할 다양한 보인 이슈는 무엇이 있을까?
리액트에서 발생하는 크로스 사이트 스크립팅 (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 · 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
}
}
}
위 예제는 서버에서 쿠키 정보를 가져와 클라이언트에서 쿠키에 유효성에 따라 작업을 처리하는 코드이다. 이 코드는 보안 관점에서 썩 좋지 못하다.
- getServerSideProps가 반환하는 props 값은 모두 사용자의 HTML에 기록된다.
- 전역 변수로 등록되어 스크립트로 충분히 접근 할 수 있어 보안 위협에 노출되는 값이 된다.
- 리다이렉트가 클라이언트에서 실행되어 성능 측면에서도 손해를 본다.
따라서, 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대 웹 애플리케이션 취약점을 공개한다.
'Study' 카테고리의 다른 글
| [React] 리액트 훅 - useRef (0) | 2024.07.26 |
|---|---|
| [Next.js 14] styled-component 사용 시 "className did not match" 에러 (0) | 2024.07.24 |
| [모던 리액트 Deep Dive 스터디] 모든 웹 개발자가 관심을 가져야 할 핵심 웹 지표 (0) | 2024.04.21 |
| [모던 리액트 Deep Dive 스터디] 리액트 17과 18의 변경사항 살펴보기 (1) | 2024.04.13 |
| [모던 리액트 Deep Dive 스터디] 모던 리액트 개발 도구로 개발 및 배포환경 구축하기 (0) | 2024.04.07 |