v-html이란?
v-text(이중 중괄호, mustaches) 디렉티브는 HTML이 아닌 일반 텍스트로 데이터를 해석한다.
따라서 아래 코드는 그대로 텍스트로 보여지게 된다.
<p>Using mustaches: {{ rawHtml }}</p>
Using mustachs: <span style="color:red">This should be red.</span> |
v-html을 사용하면 태그가 포함된 문자열을 HTML로 출력해줄 수 있다.
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
Using v-html directive: This should be red. |
v-html의 문제점
웹사이트에서 임의의 HTML을 동적으로 렌더링하면 XSS취약점으로 쉽게 이어질 수 있다. 따라서 신뢰할 수 있는 콘텐츠에서만 HTML 보간을 사용하고 사용자가 제공한 콘텐츠(유저로부터 입력받는 부분)에서는 절대 사용하면 안된다.
eslint-plugin-vue(v4.7.0 이상)를 사용 중이라면 v-html 코드에 'vue/no-v-html : Disallow use of v-html to prevent XSS attack' Essential 메세지가 뜨는 것을 확인할 수 있다.
XSS(Cross-site scripting) 공격
크로스 사이트 스크립팅은 웹사이트에 악성 스크립트를 주입하는 행위를 말합니다. 해커는 사람들이 친밀하고 안전하다고 생각하는 웹사이트에 악성 스크립트를 주입하고, 악성 스크립트가 포함된 게시글을 열람한 피해자들의 쿠키는 해커에게 전송된다. 이를 통해 해커는 피해자의 브라우저에서 스트립트를 실행해 사용자의 세션을 가로채거나, 웹사이트 변조하거나, 악의적인 컨텐츠 삽입하거나, 피싱 공격 등을 시도할 수 있게 된다. XSS 공격에는 반사형 XSS, 영구적 XSS, DOM 기반 XSS가 있는데 자세한 내용은 아래 글을 참고하자.
해결방법
공식 사이트에서 안내를 하고 있는만큼 v-html의 문제점은 명확하지만, 그럼에도 불구하고 v-html을 사용해야하는 상황이라면 해당 문자열을 한번 정제하는 작업이 필요하다.
정제 작업을 위한 라이브러리로는 Vue.js 용 라이브러리 vue-sanitize도 있지만, sanitize-html가 다운로드 수가 압도적으로 높아 해당 라이브러리를 사용해보기로 했다.
sanitize-html
htmlparser2 모듈을 기반으로 허용하려는 태그와 각 태그에 허용되는 속성을 지정하여 이외의 태그 및 속성을 정제해주는 라이브러리이다. href과 src 속성은 http, https, ftp 그리고 mailto URLs 만 포함하는 지 확인한다. 또한 host 이름을 필터링하여 특정 URL을 src iframe 태그로 허용하는 기능을 지원한다.
sanitize-html 사용방법
패키지 설치
npm install sanitize-html
or
yarn add sanitize-html
모듈 가져오기
// In ES modules
import sanitizeHtml from 'sanitize-html';
JavaScript 앱에서 사용
const dirty = 'some really tacky HTML';
const clean = sanitizeHtml(dirty);
옵션을 따로 지정해주지 않으면 default로 허용된 태그 및 속성의 목록만 허용된다.
기본 옵션은 아래와 같다.
allowedTags: [
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
"h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
"dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
"ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
"em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
"small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
"col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr"
],
disallowedTagsMode: 'discard',
allowedAttributes: {
a: [ 'href', 'name', 'target' ],
// We don't currently allow img itself by default, but
// these attributes would make sense if we did.
img: [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
},
// Lots of these won't come up by default because we don't allow them
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
// URL schemes we permit
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'tel' ],
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
allowProtocolRelative: true,
enforceHtmlBoundary: false
"기본 옵션을 따르면서, 태그나 속성 하나를 추가할 수 있나?"
기본으로 제공되는 옵션은 보안상에는 좋지만, style 속성이나 자주 이용하는 태그나 속성이 빠져있어서 사용자 커스텀이 필요한 경우가 있다. 아래 코드에서는 allowedAttributes를 지정하지 않았으므로 속성값은 기본 옵션을 따른다.
const clean = sanitizeHtml(dirty, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img' ])
});
"모든 태그 또는 속성을 허용하려면 어떻게 해야하는가?"
아래와 같이 값을 false로 설정하면 된다. 하지만 보안을 위해서는 이 속성은 사용하지 않는 것이 좋다.
allowedTags: false,
allowedAttributes: false
"모든 태그를 허용하지 않으려면 어떻게 해야하는가?"
allowedTags는 [], allowedAttributes는 {}
allowedTags: [],
allowedAttributes: {}
htmlparser2를 기반으로 하기 때문에 parser옵션을 사용해 parser 설정을 할 수 있다.
자세한 옵션의 목록은 htmlparser2 wiki를 참고한다.
sanitizer-html의 더 다양한 사용방법은 github readme를 참고한다.
sanitizer-html 예시
1. v-html 이용
input 창에 아래의 코드를 복붙해보자.
<a style='cursor:pointer;font-weight: bold;' onclick=alert(JSON.stringify(document))>나를 클릭하세요!</a>
'나를 클릭하세요!'라는 문구를 클릭하면, alert 창에 document값이 그대로 보여지게 된다.
이러한 현상이 위에서 설명한 v-html의 문제점이다.
2. sanitize-html 이용
이번에는 sanitize-html을 이용해 text 검증 과정을 추가해본 코드이다.
위와 동일하게 input 창에 코드를 복붙해본다.
<a style='cursor:pointer;font-weight: bold;' onclick=alert(JSON.stringify(document))>나를 클릭하세요!</a>
1번의 결과와는 다르게 클릭해도 아무런 반응이 일어나지 않는다.
하지만 onclick 속성 뿐만 아니라 style 속성까지 필터링해 텍스트 위에 마우스를 올려도 커서가 변하지 않는다. style 속성을 활성화하기 위해서는 커스텀 options 적용이 필요하므로, HellowWorld.vue의 22번째 줄을 주석처리하고, 23번째 줄을 주석해제 한다.
댓글