]> Untitled Git - lemmy-ui.git/commitdiff
Add nonce-based CSP header (#1907)
authorSander Saarend <sander@saarend.com>
Mon, 10 Jul 2023 18:26:41 +0000 (21:26 +0300)
committerGitHub <noreply@github.com>
Mon, 10 Jul 2023 18:26:41 +0000 (14:26 -0400)
* Remove websocket config

* Add nonce based CSP

package.json
src/server/handlers/catch-all-handler.tsx
src/server/middleware.ts
src/server/utils/create-ssr-html.tsx
src/shared/interfaces.ts

index fdc1dfdaf1522b49f5422ed87a471420187cc34e..5e3535b9049a745b482dd4b0c6c57295c9f91a81 100644 (file)
@@ -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",
index 4b0110452d41c3c7aae88c05d1620f3684b055a2..06d38f31792e441d9b4e9b6b3bb328f22ef3bc9d 100644 (file)
@@ -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);
index 0420e47e8a1dcfe67707ae3a5f99669bd8bdc764..a75d49ed7230366a0dce966b944e4e32b52386c1 100644 (file)
@@ -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();
index ba85228f2f9c1394241fe40acf63e3fd95880331..71fdb68f3559dbcc02c1ee36881282faf0d11ef1 100644 (file)
@@ -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()}
index b37dbd3ef6fb00de463a9e279ea6d38855e8da67..7beeec99943c159e04a68146c9a0d52b270388de 100644 (file)
@@ -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;
   }
 }