준영
도은
- 웹사이트의 성능만큼이나 중요한 것은 바로 웹사이트의 보안
- 코드의 규모가 증가한다는 것은 필연적으로 보안 취약점에 노출될 확률도 증가한다는 것을 의미
14.1 리액트에서 발생하는 크로스 사이트 스크립팅(XSS)
💡 크로스 사이트 스크립팅(XSS)란?
웹사이트 개발자가 아닌 제3자가 웹사이트에 악성 스크립트를 삽입해 실행할 수 있는 취약점을 의미
script
가 실행될 수 있다면, 웹사이트 개발자가 할 수 있는 모든 작업을 함께 수행 가능- 쿠키를 획득해 사용자의 로그인 세션 등을 탈취
- 사용자의 데이터를 변경하는 등 각종 위험성
14.1.1 dangerouslySetInnerHTML prop
- 특정 브라우저 DOM의 innerHTML을 특정한 내용을 교체할 수 있는 방법
- 일반적으로 게시판과 같이 사용자나 관리자가 입력한 내용을 브라우저에 표시하는 용도로 사용
function App() {
return <div dangerouslySetInnerHTML={{ __html: 'First · Second' }} />;
}
- 위험성은
dangerouslySetInnerHTML
이 인수로 받는 문자열에는 제한이 없다는 것
const html = `<span><svg/onload=alrt(origin)></span>`;
function App() {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
- 즉, 자바스크립트 코드가 들어가면 실행이 가능하다
14.1.2 useRef를 활용한 직접 삽입
- useRef를 활용하면 직접 DOM에 접근 가능 → innerHTML에 보안 취약점이 있는 스크립트를 삽입하면 동일한 문제가 발생
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} />;
}
14.1.3 리액트에서 XSS 문제를 피하는 방법
💡 리액트에서 XXS 이슈를 피하는 가장 확실한 방법은
제 3자가 삽입할 수 있는 HTML을 안전한 HTML 코드로 한 번 치환하는 것
- 이러한 과정을 새니타이즈(sanitize) 또는 이스케이프(escape)라고 하는데
- 직접 구현하는 방법도 있지만, 가장 확실한 방법은 라이브러리를 사용하는 것
import sanitizeHTML, { IOptions as SanitizeOptions } from 'sanitize-html';
// 허용하는 태그
const allowedTags = [
'div',
'p',
'span',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'figure',
'iframe',
'a',
'strong',
'i',
'br',
'img',
];
// 위 태그에서 허용할 모든 속성
const defaultAttributes = ['style', 'class'];
// 허용할 iframe 도메인
const allowedIframeDomains = ['naver.com'];
// 허용되는 태그 중 추가로 허용할 속성
const allowedAttributeForTags: {
[key in (typeof allowedTags)[number]]: Array<string>;
} = {
iframe: ['src', 'allowfullscreen', 'scrolling', 'frameborder', 'allow'],
img: ['src', 'alt'],
a: ['href'],
};
// allowedTags, allowedAttributeForTags, defaultAttributes를 기반으로 허용할 태그와 속성을 정의
const allowedAttributes = allowedTags.reduce((result, tag) => {
const additionalAttrs = allowedAttributeForTags[tag] || [];
return { ...result, [tag]: [...additionalAttrs, ...defaultAttributes] };
}, {});
- 허용할 태그와 목록을 일일히 나열하는 이른바 허용 목록(allow list) 방식을 채택하기 때문에
- 사용하기에 매우 귀찮게 느껴질 수도...
- 그러나 이렇게 허용 목록ㅇ르 작성하는 것이 훨씬 안전
- 단순히 보여줄 때뿐만 아니라 사용자가 콘텐츠를 저장할 때도 한번 이스케이프 과정을 거치는 것이 효율적이고 안전
- 애초에 XSS 위험이 있는 콘텐츠를 데이터베이스에 저장하는 것이 도움
- 이러한 치환 과정은 되도록 서버에서 수행하는 것이 좋다
- 서버는 '클라이언트에서 사용자가 입력한 데이터는 일단 의심한다'라는 자세로 POST 요청이 있는 HTML을 이스케이프 하는 것이 제일 안전
마지막으로 단순히 게시판과 같은 예시가 없다고 하더라도 XSS 문제는 충분히 발생 가능
import { useRouter } from 'next/router';
function App() {
const router = useRouter();
const query = router.query;
const html = query?.html?.toString() || '';
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
- 위와 같이 쿼리스트링에 있는 내용을 그대로 실행하거나 보여주는 경우에도 보안 취약점이 발생
14.2 getServerSideProps와 서버 컴포넌트를 주의하자
- 서버에서는 일반 사용자에게 노출되면 안 되는 정보들이 담겨 있기 때문에
- 클라이언트, 즉 브라우저에 정보를 내려줄 때는 조심해야 한다.
export default function App({ cookie }: { cookie: string }) {
if (!validateCookie(cookie)) {
Router.replace(/* ... */);
return null;
}
/* do something... */
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const cookie = ctx.req.headers.cookie || '';
return {
props: {
cookie,
},
};
};
- 예제에서는 getServerSideProps에서 cookie 정보를 가져온 다음, 이를 클라이언트 리액트 컴포넌트에 문자열로 제공해
- 클라이언트에서 쿠기의 유효성에 따라 이후 작업을 처리
- 보안에 썩 좋지 못함
- getServerSideProps가 반환하는 props 값은 모두 사용자의 HTML에 기록
- 충분히 getServerSideProps에서 처리할 수 있는 리다이렉트가 클라이언트에서 실행되어 성능 측면에서도 손해
💡 getServerSideProps가 반환하는 값 또는 서버 컴포넌트가 클라이언트 컴포넌트에 반환하는 props는
반드시 필요한 값으로만 철저하게 제한되어야 한다.
다음과 같이 수정할 수 있다.
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',
permanet: false,
},
};
}
return {
props: {
token,
},
};
};
- 쿠키 전체를 제공하는 것이 아니라 클라이언트에서 필요한 token 값만 제한적으로 제공
- 이 값을 얻을 때 예외 처리할 리다이렉트 모두 서버에서 처리
14.3 <a>
태그의 값에 적절한 제한을 둬야 한다
- 웹 개발 시에는
<a>
태그의 href에javascript:
로 시작하는 자바스크립트 코드를 넣어둔 경우를 본 적이 있을 것 <a>
태그의 기본 기능, 즉 href로 선언된 URL로 페이지를 이동하는 것을 막고 onClick이 이벤트와 같이 별도 이벤트 핸들러만 작동시키기 위한 용도로 주로 사용
function App() {
function handleClick() {
console.log('hello');
}
return (
<>
<a href="javascript:;" onClick={handleClick}>
링크
</a>
</>
);
}
- 이러한 방식은 마크업 관점에서 안티패턴
<a>
태그는 반드시 페이지 이동이 있을 때만 사용하는 것이 좋다- 페이지 이동이 없이 어떠한 핸들러만 작동시키고 싶다면
<a>
보다는 button을 사용하는 것이 좋다 - 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;
}
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>
</>
)
}
14.4 HTTP 보안 헤더 설정하기
💡 HTTP 보안 헤더
브라우저가 렌더링하는 내용과 관련된 보안 취약점을 미연에 방지하기 위해 브라우저와 함께 작동하는 헤더
- 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-XXS-Protection
- 비표준 기술. 현재 사파리와 구형 브라우저에서만 제공되는 기능
💡 페이지에서 XSS 취약점이 발견되면 페이지 로딩을 중단하는 헤더
- Content-Security-Policy를 지원하지 않는 구형 브라우저에서는 사용이 가능
- 그러나 이 헤더를 전적으로 믿어서는 안 되며, 반드시 페이지 내부에서 XSS에 대한 처리가 존재하는 것이 좋다
14.4.3 X-Frame-Options
frame
,iframe
,embed
,object
내부에서 렌더링을 허용할지를 나타낼 수 있다.- 페이지에서 네이버를 iframe으로 렌더링한다고 가정해보자
- 사용자는 이 페이지를 네이버로 오해할 수 있고
- 공격자를 이를 활용해 사용자의 개인정보를 탈취 가능
💡 X-Frame-Options는 외부에서 자신의 페이지를 위와 같은 방식으로 삽입되는 것을 막아주는 헤더
- 네이버 같은 경우에도,
X-Frame-Options: deny
옵션이 있어서<iframe>
으로 삽입되는 것을 막는 역할
14.4.4 Permissions-Policy
💡 웹사이트에서 사용할 수 있는 기능과 사용할 수 없는 기능을 명시적으로 선언하는 헤더
- 개발자는 다양한 브라우저의 기능이나 API를 선택적으로 활성화하거나 필요에 따라서는 비활성화할 수 있다.
- 여기서 말하는 기능은 카메라나 GPS와 같이 브라우저가 제공하는 기능
- 브라우저에서 사용자의 위치를 확인하는 기능과 관련된 코드를 전혀 작성하지 않았다고 해보자
- XSS 공격 등으로 인해 이 기능을 취득해서 사용하게 되면, 사용자의 위치를 획득할 수 있게 되는 것
14.4.5 X-Content-Type-Options
- MIME(Multipurpose Internet Mail Extensions): Content-type의 값으로 사용
- 메일을 전송할 때 사용하던 인코딩 방식으로
- 현재는 Content-type에서 대표적으로 사용
💡 MIME 유형이 브라우저에 의해 임의로 변경되지 않게 하는 헤더
Content-type: text/css
헤더가 없는 파일은 브라우저가 임의로 CSS로 사용할 수 없으며Content-type: text/javascript
나Content-type: application/javascript
헤더가 없는 파일은 자바스크립트로 해석할 수 없다.- 즉, 웹서버가 브라우저에 강제로 이 파일을 읽는 방식을 지정하는 것