Add nonce-based CSP header (#1907)

* Remove websocket config

* Add nonce based CSP
This commit is contained in:
Sander Saarend 2023-07-10 21:26:41 +03:00 committed by GitHub
parent 462bdb10c9
commit 546f0ad704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 31 additions and 17 deletions

View 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",

View 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);

View 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();

View 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()}

View 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;
}
}