From: Sander Saarend <sander@saarend.com> Date: Mon, 10 Jul 2023 18:26:41 +0000 (+0300) Subject: Add nonce-based CSP header (#1907) X-Git-Url: http://these/git/%7B%60/feeds/c/%7BimageSrc%7D?a=commitdiff_plain;h=546f0ad704940aafc5c2bac244f0f150eb7a47cc;p=lemmy-ui.git Add nonce-based CSP header (#1907) * Remove websocket config * Add nonce based CSP --- diff --git a/package.json b/package.json index fdc1dfd..5e3535b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "analyze": "webpack --mode=none", "prebuild:dev": "yarn clean && node generate_translations.js", - "build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development", + "build:dev": "webpack --env LEMMY_UI_DISABLE_CSP=true --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development", "prebuild:prod": "yarn clean && node generate_translations.js", "build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production", "clean": "yarn run rimraf dist", diff --git a/src/server/handlers/catch-all-handler.tsx b/src/server/handlers/catch-all-handler.tsx index 4b01104..06d38f3 100644 --- a/src/server/handlers/catch-all-handler.tsx +++ b/src/server/handlers/catch-all-handler.tsx @@ -120,7 +120,7 @@ export default async (req: Request, res: Response) => { const root = renderToString(wrapper); - res.send(await createSsrHtml(root, isoData)); + res.send(await createSsrHtml(root, isoData, res.locals.cspNonce)); } catch (err) { // If an error is caught here, the error page couldn't even be rendered console.error(err); diff --git a/src/server/middleware.ts b/src/server/middleware.ts index 0420e47..a75d49e 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,3 +1,4 @@ +import * as crypto from "crypto"; import type { NextFunction, Request, Response } from "express"; import { hasJwtCookie } from "./utils/has-jwt-cookie"; @@ -8,9 +9,20 @@ export function setDefaultCsp({ res: Response; next: NextFunction; }) { + res.locals.cspNonce = crypto.randomBytes(16).toString("hex"); + res.setHeader( "Content-Security-Policy", - `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src * data:` + `default-src 'self'; + manifest-src *; + connect-src *; + img-src * data:; + script-src 'self' 'nonce-${res.locals.cspNonce}'; + style-src 'self' 'unsafe-inline'; + form-action 'self'; + base-uri 'self'; + frame-src *; + media-src * data:`.replace(/\s+/g, " ") ); next(); diff --git a/src/server/utils/create-ssr-html.tsx b/src/server/utils/create-ssr-html.tsx index ba85228..71fdb68 100644 --- a/src/server/utils/create-ssr-html.tsx +++ b/src/server/utils/create-ssr-html.tsx @@ -4,7 +4,7 @@ import { renderToString } from "inferno-server"; import serialize from "serialize-javascript"; import sharp from "sharp"; import { favIconPngUrl, favIconUrl } from "../../shared/config"; -import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces"; +import { IsoDataOptionalSite } from "../../shared/interfaces"; import { buildThemeList } from "./build-themes-list"; import { fetchIconPng } from "./fetch-icon-png"; @@ -14,7 +14,8 @@ let appleTouchIcon: string | undefined = undefined; export async function createSsrHtml( root: string, - isoData: IsoDataOptionalSite + isoData: IsoDataOptionalSite, + cspNonce: string ) { const site = isoData.site_res; @@ -22,6 +23,12 @@ export async function createSsrHtml( (await buildThemeList())[0] }.css" />`; + const customHtmlHeaderScriptTag = new RegExp("<script", "g"); + const customHtmlHeaderWithNonce = customHtmlHeader.replace( + customHtmlHeaderScriptTag, + `<script nonce="${cspNonce}"` + ); + if (!appleTouchIcon) { appleTouchIcon = site?.site_view.site.icon ? `data:image/png;base64,${await sharp( @@ -45,28 +52,28 @@ export async function createSsrHtml( process.env["LEMMY_UI_DEBUG"] === "true" ? renderToString( <> - <script src="//cdn.jsdelivr.net/npm/eruda"></script> - <script>eruda.init();</script> + <script + nonce={cspNonce} + src="//cdn.jsdelivr.net/npm/eruda" + ></script> + <script nonce={cspNonce}>eruda.init();</script> </> ) : ""; const helmet = Helmet.renderStatic(); - const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST }; - return ` <!DOCTYPE html> <html ${helmet.htmlAttributes.toString()}> <head> - <script>window.isoData = ${serialize(isoData)}</script> - <script>window.lemmyConfig = ${serialize(config)}</script> + <script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script> <!-- A remote debugging utility for mobile --> ${erudaStr} <!-- Custom injected script --> - ${customHtmlHeader} + ${customHtmlHeaderWithNonce} ${helmet.title.toString()} ${helmet.meta.toString()} diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index b37dbd3..7beeec9 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -18,14 +18,9 @@ export type IsoDataOptionalSite<T extends RouteData = any> = Partial< > & Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>; -export interface ILemmyConfig { - wsHost?: string; -} - declare global { interface Window { isoData: IsoData; - lemmyConfig?: ILemmyConfig; } }