diff --git a/src/client/app/emotionCache.ts b/src/client/app/emotionCache.ts new file mode 100644 index 0000000000..cac1f322ac --- /dev/null +++ b/src/client/app/emotionCache.ts @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import createCache from '@emotion/cache'; + +//creates the nonce for the script being run +//the nonce works alongside the webpack_nonce and plotly_nonce to protect against unwanted scripts +const nonce = (document.querySelector('script[nonce]') as HTMLScriptElement | null)?.nonce; + +const emotionCache = createCache({ + key: 'css', + nonce: nonce +}); + +export default emotionCache; diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 83dfee7ad8..8a1d9c6465 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -10,6 +10,38 @@ import { store } from './store'; import RouteComponent from './components/RouteComponent'; import { initApp } from './redux/slices/appStateSlice'; import './styles/index.css'; +import { CacheProvider } from '@emotion/react'; +import emotionCache from './emotionCache'; + +//these lines take the nonce, and create the webpack and plotly nonces from it +//these are additional nonces that contribute to styling with webpack and plotly +const __webpack_nonce__ = (document.querySelector('script[nonce]') as HTMLScriptElement | null)?.nonce; +(window as any).__webpack_nonce__ = __webpack_nonce__; +(window as any).__plotly_nonce__ = __webpack_nonce__; + +declare global { + interface Window { + __webpack_nonce__?: string; + __plotly_nonce__?: string; + } +} + + +const originalAppendChild = document.head.appendChild; +document.head.appendChild = function (node: any) { + if ( + node instanceof HTMLStyleElement + ) { + node.setAttribute('nonce', __webpack_nonce__ || ''); + } + + try { + return originalAppendChild.call(this, node); + } catch (err) { + console.error('Failed to append style:', err); + throw err; + } +}; store.dispatch(initApp()); @@ -19,7 +51,9 @@ const root = createRoot(container); root.render( // Provides the Redux store to all child components - < Provider store={store} stabilityCheck='always' > - < RouteComponent /> - + + + + + ); diff --git a/src/client/index.html b/src/client/index.html index 2d2a3d678f..c4b8d0dfc8 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -7,21 +7,47 @@ - - - - Open Energy Dashboard - - + + + + + Open Energy Dashboard + + + + + + -
- - +
+ + + + - + + \ No newline at end of file diff --git a/src/server/app.js b/src/server/app.js index 0dc5fdbc26..9f35182cd9 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -33,6 +33,7 @@ const units = require('./routes/units'); const conversions = require('./routes/conversions'); const ciks = require('./routes/ciks'); const { HTTP_CODES } = require('./util/httpCodes'); +const crypto = require('node:crypto'); // Detect test environment and use higher rate limits during tests. // Rate limiting is critical for security in production but interferes with automated testing. @@ -162,6 +163,13 @@ router.get('*', (req, res) => { const subdir = config.subdir || '/'; let htmlPlusData = html.toString().replace('SUBDIR', subdir); + + //assigns a value to the nonce in order to check for authenticity + const nonce = crypto.randomBytes(16).toString('base64url'); + htmlPlusData = htmlPlusData.replace(/{{nonce}}/g, nonce); + + res.setHeader('Content-Security-Policy', `default-src 'self'; img-src 'self' data: ; font-src 'self' https://maxcdn.bootstrapcdn.com ; media-src 'self'; script-src 'self' 'nonce-${nonce}' ; style-src 'self' 'nonce-${nonce}' 'unsafe-inline';`) + res.send(htmlPlusData); }); }); diff --git a/webpack.config.js b/webpack.config.js index 8ff0f7d647..c2d3a1f921 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); -const NodePolyfillPlugin = require('node-polyfill-webpack-plugin') +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); const webpack = require('webpack'); const path = require('path'); @@ -40,12 +40,22 @@ const config = { // All TypeScript ('.ts' or '.tsx') will be handled by 'awesome-typescript-loader'. { test: /\.[jt]sx?$/, exclude: /node_modules/, use: 'ts-loader' }, // CSS stylesheet loader. - { test: /\.css$/, use: [ - {loader: 'style-loader'}, - {loader: 'css-loader'} - ] }, + { + test: /\.css$/, use: [ + { + loader: 'style-loader', + options: { + attributes: { + //this line allows the webpack nonce to be applied to styles + nonce: '__webpack_nonce__' + } + } + }, + { loader: 'css-loader' } + ] + }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. - { enforce: 'pre', test: /\.js$/, use:[{loader: 'source-map-loader'}] } + { enforce: 'pre', test: /\.js$/, use: [{ loader: 'source-map-loader' }] } ] }, output: {