yumechi-no-kuni/packages/backend/src/server/csp.ts
eternal-flame-AD e0c0f9142c
Some checks failed
Publish Docker image / Build (push) Has been cancelled
Lint / pnpm_install (push) Has been cancelled
Test (backend) / unit (22.11.0) (push) Has been cancelled
Test (production install and build) / production (22.11.0) (push) Has been cancelled
Lint / lint (backend) (push) Has been cancelled
Lint / lint (frontend) (push) Has been cancelled
Lint / lint (frontend-embed) (push) Has been cancelled
Lint / lint (frontend-shared) (push) Has been cancelled
Lint / lint (misskey-bubble-game) (push) Has been cancelled
Lint / lint (misskey-js) (push) Has been cancelled
Lint / lint (misskey-reversi) (push) Has been cancelled
Lint / lint (sw) (push) Has been cancelled
Lint / typecheck (backend) (push) Has been cancelled
Lint / typecheck (misskey-js) (push) Has been cancelled
Lint / typecheck (sw) (push) Has been cancelled
revert connect-src CSP
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-05-11 22:11:22 -05:00

110 lines
3 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import { URL } from 'url';
export type CSPHashed = {
content: string,
integrity: string,
};
export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
url: string | URL,
mediaProxy?: string,
script_src?: string[],
append?: { [key: string]: string | string[] },
}) {
const parsedUrl = new URL(options.url);
const localPath = (path: string, scheme?: string) => {
const finalUrl = new URL(path, parsedUrl.origin);
if (scheme) {
finalUrl.protocol = scheme;
}
return finalUrl.toString();
};
const keys = Array.from(hashedMap.keys());
const scripts = keys
.filter(name => name.endsWith('.js'))
.map(name => `'${hashedMap.get(name)!.integrity}'`);
const styles = keys
.filter(name => name.endsWith('.css'))
.map(name => `'${hashedMap.get(name)!.integrity}'`);
const cpolicy = [
['default-src', ['\'self\'']],
['img-src',
[
'\'self\'',
'data:',
'blob:',
// 'https://avatars.githubusercontent.com', // uncomment this for contributor avatars to work
options.mediaProxy,
].filter(Boolean)],
['media-src', ['\'self\'', 'data:', options.mediaProxy].filter(Boolean)],
['font-src', ['\'self\'']],
['style-src', ['\'self\'', ...styles]],
// remove below line if you do not use code highlighting.
// shiki is not designed to work with CSP, so we need to allow unsafe-inline for now.
//
// Allowing inline style attributes is a very small risk, so I will allow by default.
// Since you can not write CSS selectors or cascading rules in the inline style attributes.
//
// ref: https://github.com/shikijs/shiki/issues/671
['style-src-attr', ['\'self\'', '\'unsafe-inline\'']],
['script-src', [
...(options.script_src ?? ['\'self\'']),
'\'wasm-unsafe-eval\'',
...scripts,
]],
['object-src', ['\'none\'']],
['base-uri', ['\'none\'']],
['form-action', ['\'self\'']],
['child-src', ['\'self\'']],
['manifest-src', ['\'self\'']],
...(process.env.NODE_ENV === 'production' ?
[
['upgrade-insecure-requests', []],
] : []),
] as [string, string[]][];
if (options.append) {
for (const [name, values] of Object.entries(options.append)) {
if (!values) {
continue;
}
const found = cpolicy.find(([n]) => n === name);
if (found) {
found[1].push(...(Array.isArray(values) ? values : [values]));
} else {
cpolicy.push([name, Array.isArray(values) ? values : [values]]);
}
}
}
return cpolicy
.map(([name, values]) => {
return `${name} ${values.join(' ')}`;
}).join('; ');
}
export function hashResource(res: string): CSPHashed {
const sha256 = createHash('sha256');
sha256.update(res);
const content = res;
const integrity = `sha256-${sha256.digest('base64')}`;
return { content, integrity };
}
export function hashSourceFile(file: string): CSPHashed {
const content = readFileSync(file, 'utf8');
return hashResource(content);
}