Reactで安全にMarkdownをHTMLレンダリングする

ユーザー入力値をMarkdownとして受取り、HTMLとしてレンダリングしたい場面は少なくない。 当然ながらHTMLとして出力するため、onerrorやhref属性などは適切にサニタイズしなければならない。 import React, { Component } from 'react'; import marked from 'marked'; import PropTypes from 'prop-types'; const markdown = (data) => { return marked(data) } const SanitizedOutput = (props) => { const { data } = props; return (<div dangerouslySetInnerHTML={{__html: markdown(data)}}></div>) } SanitizedOutput.propTypes = { data: PropTypes.string.isRequired } export default SanitizedOutput; このとき、dataが<img src='x' onerror='alert(1)'>だと当然ながらXSSが生じる。 <div> <img src='x' onerror='alert(1)'> </div> sanitized-htmlを使えば柔軟に使用できるタグや属性を指定できる。 例えば、imgタグを許可し、imgタグのsrc属性のみを許可したい時は以下のようになる。 import React, { Component } from 'react'; import marked from 'marked'; import sanitize from 'sanitize-html'; import PropTypes from 'prop-types'; const markdown = (data) => { return sanitize(marked(data), { allowedTags: sanitize.

最近のJavaScriptフレームワークでのXSS

所謂、最近流行っているJavaScriptフレームワークでのXSSの例をいくつか挙げようと思う。 最近のJavaScriptフレームワークは賢いので、データをバインドする際にHTMLエスケープしてくれてXSSから保護してくれる。 しかしながら、保護が適用されないケースもあるため、過度にフレームワークに信頼しているとXSSを作り込んでしまう。 ここではReactとVue.jsでその例をいくつか挙げようと思う。 あくまで「やってしまうかもしれない例」というだけで、ReactやVue.jsの固有の問題というわけではないです。 React javascript:スキーム aタグのhref属性にjavascript:やdata:text/htmlなどの任意のスキームをユーザーが設定できる場合、XSSが生じる。 これはReactに限った話ではなく、古くからある手法だが、未だにたまに見る。 import React, { Component } from 'react'; class App extends Component { render() { return ( <div className="App"> <a href={this.props.link}>{this.props.link}</a> </div> ); } } ReactDOM.render(<App link="javascript:alert(1)" />, document.getElementById('root')); 対策としてはスキームをhttp://かhttps://のみに制限するなどする。 ユーザーが任意のpropsを注入できる場合 https://hackerone.com/reports/49652 にあるように、ユーザーの入力値をそのままReact.createElementのpropsとして渡した場合などにはXSSが生じる。 import React from 'react'; import ReactDOM from 'react-dom'; // const props = JSON.parse('{"name": "foo"}'); const props = JSON.parse('{"name": {"dangerouslySetInnerHTML" : { "__html": "<svg/onload=alert(1)>"}}}'); ReactDOM.render(React.createElement("p", props.name), document.getElementById('root')) dangerouslySetInnerHTML はい。 https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml