|
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