|
2 | 2 | import PropTypes from 'prop-types'
|
3 | 3 | import React, { PureComponent } from 'react'
|
4 | 4 | import serialize from 'serialize-javascript'
|
| 5 | +import { Helmet } from 'react-helmet' |
| 6 | +import DOMPurify from 'dompurify' |
5 | 7 |
|
6 | 8 | // @twreporter
|
7 | 9 | import webfonts from '@twreporter/react-components/lib/text/utils/webfonts'
|
@@ -110,34 +112,51 @@ export default class Html extends PureComponent {
|
110 | 112 | {styleElement}
|
111 | 113 | </head>
|
112 | 114 | <body>
|
113 |
| - <div id="root" dangerouslySetInnerHTML={{ __html: contentMarkup }} /> |
| 115 | + {/* SECURITY: contentMarkup is trusted server-rendered React content. |
| 116 | + Any modifications to the content generation pipeline must maintain XSS protections. */} |
| 117 | + <div id="root" |
| 118 | + dangerouslySetInnerHTML={{ |
| 119 | + __html: DOMPurify.sanitize(contentMarkup, { |
| 120 | + FORBID_TAGS: ['script'], |
| 121 | + FORBID_ATTR: ['onerror', 'onload', 'onclick'] |
| 122 | + }) |
| 123 | + }} |
| 124 | + /> |
114 | 125 | <script
|
115 | 126 | defer
|
116 | 127 | src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.zh-Hant-TW"
|
117 | 128 | />
|
118 |
| - <script |
119 |
| - dangerouslySetInnerHTML={{ |
120 |
| - __html: `window.__REDUX_STATE__=${serialize(store.getState())};`, |
121 |
| - }} |
122 |
| - charSet="UTF-8" |
123 |
| - /> |
| 129 | + <Helmet> |
| 130 | + <script |
| 131 | + id="redux-state" |
| 132 | + type="application/json" |
| 133 | + dangerouslySetInnerHTML={{ |
| 134 | + __html: JSON.stringify(serialize(store.getState(), { |
| 135 | + isJSON: true, |
| 136 | + isScriptSafe: true, |
| 137 | + escapeHTML: true |
| 138 | + })) |
| 139 | + }} |
| 140 | + /> |
| 141 | + <script> |
| 142 | + {` |
| 143 | + window.__REDUX_STATE__ = JSON.parse(document.getElementById('redux-state').textContent); |
| 144 | + `} |
| 145 | + </script> |
| 146 | + </Helmet> |
124 | 147 | {_.map(scripts, (script, key) => (
|
125 | 148 | <script src={script} key={'scripts' + key} charSet="UTF-8" />
|
126 | 149 | ))}
|
127 | 150 | {scriptElement}
|
128 |
| - <script |
129 |
| - dangerouslySetInnerHTML={{ |
130 |
| - __html: `(function(d) { |
131 |
| - var config = { |
132 |
| - kitId: 'vlk1qbe', |
133 |
| - scriptTimeout: 3000, |
134 |
| - async: true |
135 |
| - }, |
136 |
| - h=d.documentElement,t=setTimeout(function(){h.className=h.className.replace(/\bwf-loading\b/g,"")+" wf-inactive";},config.scriptTimeout),tk=d.createElement("script"),f=false,s=d.getElementsByTagName("script")[0],a;h.className+=" wf-loading";tk.src='https://use.typekit.net/'+config.kitId+'.js';tk.async=true;tk.onload=tk.onreadystatechange=function(){a=this.readyState;if(f||a&&a!="complete"&&a!="loaded")return;f=true;clearTimeout(t);try{Typekit.load(config)}catch(e){}};s.parentNode.insertBefore(tk,s) |
137 |
| - })(document); |
138 |
| - `, |
139 |
| - }} |
140 |
| - /> |
| 151 | + <Helmet> |
| 152 | + <script |
| 153 | + src="https://use.typekit.net/vlk1qbe.js" |
| 154 | + async |
| 155 | + /> |
| 156 | + <script> |
| 157 | + {`try{Typekit.load({kitId: 'vlk1qbe', scriptTimeout: 3000, async: true})}catch(e){}`} |
| 158 | + </script> |
| 159 | + </Helmet> |
141 | 160 | </body>
|
142 | 161 | </html>
|
143 | 162 | )
|
|
0 commit comments