mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-25 13:51:13 +00:00
Add nonce-based CSP header (#1907)
* Remove websocket config * Add nonce based CSP
This commit is contained in:
parent
1d73a57297
commit
27222a4f30
5 changed files with 31 additions and 17 deletions
|
@ -8,7 +8,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "webpack --mode=none",
|
"analyze": "webpack --mode=none",
|
||||||
"prebuild:dev": "yarn clean && node generate_translations.js",
|
"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",
|
"prebuild:prod": "yarn clean && node generate_translations.js",
|
||||||
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
|
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
|
||||||
"clean": "yarn run rimraf dist",
|
"clean": "yarn run rimraf dist",
|
||||||
|
|
|
@ -120,7 +120,7 @@ export default async (req: Request, res: Response) => {
|
||||||
|
|
||||||
const root = renderToString(wrapper);
|
const root = renderToString(wrapper);
|
||||||
|
|
||||||
res.send(await createSsrHtml(root, isoData));
|
res.send(await createSsrHtml(root, isoData, res.locals.cspNonce));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If an error is caught here, the error page couldn't even be rendered
|
// If an error is caught here, the error page couldn't even be rendered
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import * as crypto from "crypto";
|
||||||
import type { NextFunction, Request, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
||||||
|
|
||||||
|
@ -8,9 +9,20 @@ export function setDefaultCsp({
|
||||||
res: Response;
|
res: Response;
|
||||||
next: NextFunction;
|
next: NextFunction;
|
||||||
}) {
|
}) {
|
||||||
|
res.locals.cspNonce = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Security-Policy",
|
"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();
|
next();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderToString } from "inferno-server";
|
||||||
import serialize from "serialize-javascript";
|
import serialize from "serialize-javascript";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { favIconPngUrl, favIconUrl } from "../../shared/config";
|
import { favIconPngUrl, favIconUrl } from "../../shared/config";
|
||||||
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
import { IsoDataOptionalSite } from "../../shared/interfaces";
|
||||||
import { buildThemeList } from "./build-themes-list";
|
import { buildThemeList } from "./build-themes-list";
|
||||||
import { fetchIconPng } from "./fetch-icon-png";
|
import { fetchIconPng } from "./fetch-icon-png";
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ let appleTouchIcon: string | undefined = undefined;
|
||||||
|
|
||||||
export async function createSsrHtml(
|
export async function createSsrHtml(
|
||||||
root: string,
|
root: string,
|
||||||
isoData: IsoDataOptionalSite
|
isoData: IsoDataOptionalSite,
|
||||||
|
cspNonce: string
|
||||||
) {
|
) {
|
||||||
const site = isoData.site_res;
|
const site = isoData.site_res;
|
||||||
|
|
||||||
|
@ -22,6 +23,12 @@ export async function createSsrHtml(
|
||||||
(await buildThemeList())[0]
|
(await buildThemeList())[0]
|
||||||
}.css" />`;
|
}.css" />`;
|
||||||
|
|
||||||
|
const customHtmlHeaderScriptTag = new RegExp("<script", "g");
|
||||||
|
const customHtmlHeaderWithNonce = customHtmlHeader.replace(
|
||||||
|
customHtmlHeaderScriptTag,
|
||||||
|
`<script nonce="${cspNonce}"`
|
||||||
|
);
|
||||||
|
|
||||||
if (!appleTouchIcon) {
|
if (!appleTouchIcon) {
|
||||||
appleTouchIcon = site?.site_view.site.icon
|
appleTouchIcon = site?.site_view.site.icon
|
||||||
? `data:image/png;base64,${await sharp(
|
? `data:image/png;base64,${await sharp(
|
||||||
|
@ -45,28 +52,28 @@ export async function createSsrHtml(
|
||||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||||
? renderToString(
|
? renderToString(
|
||||||
<>
|
<>
|
||||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
<script
|
||||||
<script>eruda.init();</script>
|
nonce={cspNonce}
|
||||||
|
src="//cdn.jsdelivr.net/npm/eruda"
|
||||||
|
></script>
|
||||||
|
<script nonce={cspNonce}>eruda.init();</script>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const helmet = Helmet.renderStatic();
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
||||||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html ${helmet.htmlAttributes.toString()}>
|
<html ${helmet.htmlAttributes.toString()}>
|
||||||
<head>
|
<head>
|
||||||
<script>window.isoData = ${serialize(isoData)}</script>
|
<script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script>
|
||||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
|
||||||
|
|
||||||
<!-- A remote debugging utility for mobile -->
|
<!-- A remote debugging utility for mobile -->
|
||||||
${erudaStr}
|
${erudaStr}
|
||||||
|
|
||||||
<!-- Custom injected script -->
|
<!-- Custom injected script -->
|
||||||
${customHtmlHeader}
|
${customHtmlHeaderWithNonce}
|
||||||
|
|
||||||
${helmet.title.toString()}
|
${helmet.title.toString()}
|
||||||
${helmet.meta.toString()}
|
${helmet.meta.toString()}
|
||||||
|
|
|
@ -18,14 +18,9 @@ export type IsoDataOptionalSite<T extends RouteData = any> = Partial<
|
||||||
> &
|
> &
|
||||||
Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
|
Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
|
||||||
|
|
||||||
export interface ILemmyConfig {
|
|
||||||
wsHost?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
isoData: IsoData;
|
isoData: IsoData;
|
||||||
lemmyConfig?: ILemmyConfig;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue