Skip to content
This repository was archived by the owner on Feb 20, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions library/lib/SafeScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const crypto = require('crypto')
const React = require('react')

module.exports = { SafeScript, recordScriptHashes }

function SafeScript({ dangerouslySetInnerHTML }) {
if (!global.scriptHashes) throw new Error('No script hashes present')
global.scriptHashes.add(crypto.createHash('sha256').update(dangerouslySetInnerHTML.__html).digest('base64'))
return React.createElement('script', { dangerouslySetInnerHTML })
}

function recordScriptHashes(newScriptHashses, callback) {
global.scriptHashes = newScriptHashses
const result = callback()
global.scriptHashes = null
return result
}
10 changes: 8 additions & 2 deletions library/lib/eval-in-fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ attempt(() => {
function handleMessage(source) {
attempt(() => {
process.off('message', handleMessage)
const { template, renderer } = evalSource(source)
const result = renderer(template)

const { template, renderer, recordScriptHashes } = evalSource(source)
const scriptHashes = new Set()
const result = recordScriptHashes(scriptHashes, () =>
renderer(template)
)
if (scriptHashes.size)
console.log('[Warning]: generating a static template with inline scripts, if you want to use a CSP header, make it a dynamic template by exporting a function')

process.send(result, e =>
attempt(() => {
Expand Down
3 changes: 2 additions & 1 deletion library/lib/rollbar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs'
import merge from 'rollbar/src/merge'
import { SafeScript } from './SafeScript'

const snippet = fs.readFileSync(__non_webpack_require__.resolve('rollbar/dist/rollbar.snippet.js'), 'utf8')

Expand All @@ -15,5 +16,5 @@ const defaultOptions = {
export default function rollbar(options, nonSerializableRollbarConfig = '/* no non-serializable config */') {
const config = JSON.stringify(merge(defaultOptions, options))
const __html = `var _rollbarConfig = ${config};${nonSerializableRollbarConfig};${snippet}`
return <script dangerouslySetInnerHTML={{ __html }} />
return <SafeScript dangerouslySetInnerHTML={{ __html }} />
}
69 changes: 58 additions & 11 deletions library/lib/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,20 @@ const envRequire = isProduction ? require : require('import-fresh')

const notCached = ['html', 'txt', 'json', 'xml']

const { contentSecurityPolicy, ...helmetOptionsToUse } = helmetOptions || {}
const cspMiddleware = contentSecurityPolicy && createCspMiddleware(contentSecurityPolicy)
const [sendHtmlFunction, sendHtmlFunctionIsAsync] = cspMiddleware
? [sendHtmlWithCspHeaders, true]
: [sendHtml, false]

if (isProduction) app.use(morgan('combined'))
app.use(helmet(Object.assign(
{
hsts: false, // hsts-headers are sent by our loadbalancer
contentSecurityPolicy: false,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit strange that we have

const { contentSecurityPolicy, ...helmetOptionsToUse } = helmetOptions || {}

and

 Object.assign( {
    hsts: false, // hsts-headers are sent by our loadbalancer
    contentSecurityPolicy: false,
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  },helmetOptionsToUse)

Does that mean that the resulting object has always contentSecurityPolicy: false?

referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
},
helmetOptions
helmetOptionsToUse
)))
app.use(compression())
app.set('trust proxy', true)
Expand Down Expand Up @@ -138,28 +144,27 @@ function serveIndexWithRouting(file, req, res, next) {

const location = parsePath(req.url)

const [dataOrPromise, template] = getDataAndRouteTemplate(routeTemplate, location, req)
const [dataOrPromiseFromTemplate, template] = getDataAndRouteTemplate(routeTemplate, location, req)

const dataOrPromise =
dataOrPromiseFromTemplate.then ? dataOrPromiseFromTemplate :
sendHtmlFunctionIsAsync ? Promise.resolve(dataOrPromiseFromTemplate) :
dataOrPromiseFromTemplate

if (dataOrPromise.then)
dataOrPromise
.then(({ status, headers, data }) => {
const html = template({ location, data })
res.status(status).set(headers).send(html)
})
.then(data => sendHtmlFunction(req, res, template, location, data))
.catch(error => {
reportServerError(error, req)
serveInternalServerError(error, req, res, next)
})
else {
else
try {
const { data, status, headers } = dataOrPromise
const html = template({ location, data })
res.status(status).set(headers).send(html)
sendHtmlFunction(req, res, template, location, dataOrPromise)
} catch (error) {
reportServerError(error, req)
serveInternalServerError(error, req, res, next)
}
}
}

function getDataAndRouteTemplate(routeTemplate, location, req) {
Expand Down Expand Up @@ -191,3 +196,45 @@ function reportServerError(error, req) {
console.error(error)
if (reportError) reportError(error, req)
}


async function sendHtml(req, res, template, location, { status, headers, data }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not have to be async right?

const scriptHashes = new Set()
const html = template({ location, data }, scriptHashes)
res.status(status).set(headers).send(html)
}

async function sendHtmlWithCspHeaders(req, res, template, location, { status, headers, data }) {
const scriptHashes = new Set()
const html = template({ location, data }, scriptHashes)

// make script hashes available for CSP middleware
res.locals.scriptHashes = Array.from(scriptHashes).map(hash => `'sha256-${hash}'`)
await addCspHeaders(req, res)

res.status(status).set(headers).send(html)
}

function createCspMiddleware(contentSecurityPolicy) {
const contentSecurityPolicyWithHashes = {
...contentSecurityPolicy,
directives: {
...contentSecurityPolicy.directives,
'script-src-elem': [
...contentSecurityPolicy.directives['script-src-elem'],
(req, res) => res.locals.scriptHashes.join(' '),
]
}
}
return helmet.contentSecurityPolicy(contentSecurityPolicyWithHashes)
}

function addCspHeaders(req, res) {
return new Promise((resolve, reject) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason we need to wrap this in a Promise? And make 'sendHtmlWithCspHeaders' async?
For me it seems that the template rendering is just a bit more involved (namely collecting the extra script SHA's), but no extra async work?

cspMiddleware(req, res, (e) => {
if (e) reject(e)
else resolve(undefined)
})
})
}

5 changes: 3 additions & 2 deletions library/lib/stylesheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import hotCssReplacementClient from './hot-css-replacement-client?transpiled-javascript-string'
import { SafeScript } from './SafeScript'

const isWatch = process.env.WATCH

Expand All @@ -18,10 +19,10 @@ export default __webpack_css_chunk_hashes__
function createHotReloadClient() {
const [ port, cssHashes, chunkName, publicPath ] = [ __webpack_websocket_port__, __webpack_css_chunk_hashes__, __webpack_chunkname__, __webpack_public_path__ ]
return (
<script
<SafeScript
key='stylesheet_hotCssReplacementClient'
dangerouslySetInnerHTML={{
__html: `(${hotCssReplacementClient})(${port}, ${JSON.stringify(cssHashes)}, '${chunkName}', '${publicPath}')`
__html: `(${hotCssReplacementClient})(${port}, ${JSON.stringify(cssHashes)}, '${chunkName}', '${publicPath}')`
}}
/>
)
Expand Down
3 changes: 2 additions & 1 deletion library/lib/universalComponents.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hydrateRoot, createRoot } from 'react-dom/client'
import { safeJsonStringify } from '@kaliber/safe-json-stringify'
import { SafeScript } from './SafeScript'

const containerMarker = 'data-kaliber-component-container'
const E = /** @type {any} */ ('kaliber-component-container')
Expand All @@ -19,7 +20,7 @@ export function ComponentServerWrapper({ componentName, props, renderedComponent

{/* Use render blocking script to set a container marker and remove the custom components.
This ensures the page is never rendered with the intermediate structure */}
<script dangerouslySetInnerHTML={{ __html: scriptContent }} />
<SafeScript dangerouslySetInnerHTML={{ __html: scriptContent }} />
</>
)
}
Expand Down
1 change: 1 addition & 0 deletions library/webpack-loaders/template-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ TemplateLoader.pitch = function TemplateLoaderPitch(remainingRequest, precedingR
// This should tell us what we need to use: https://webpack.js.org/configuration/module/#rule-enforce
return `|export { default as template } from '-!${precedingRequest}!${remainingRequest}?template-source'
|export { default as renderer } from '${rendererPath}'
|export { recordScriptHashes } from '@kaliber/build/lib/SafeScript'
|`.split(/^[ \t]*\|/m).join('')
}
8 changes: 5 additions & 3 deletions library/webpack-plugins/template-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,16 @@ async function createStaticTemplate(name, source, map) {
function createDynamicTemplate(name, ext) {
return new RawSource(
`|const envRequire = process.env.NODE_ENV === 'production' ? require : require('import-fresh')
|const { template, renderer } = envRequire('./${name}${ext}')
|const { template, renderer, recordScriptHashes } = envRequire('./${name}${ext}')
|
|Object.assign(render, template)
|
|module.exports = render
|
|function render(props) {
| return renderer(template(props))
|function render(props, scriptHashes) {
| return recordScriptHashes(scriptHashes, () =>
| renderer(template(props))
| )
|}
|`.replace(/^[ \t]*\|/gm, '')
)
Expand Down