"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",
+import * as crypto from "crypto";
import type { NextFunction, Request, Response } from "express";
import { hasJwtCookie } from "./utils/has-jwt-cookie";
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();
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";
export async function createSsrHtml(
root: string,
- isoData: IsoDataOptionalSite
+ isoData: IsoDataOptionalSite,
+ cspNonce: string
) {
const site = isoData.site_res;
(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(
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()}