Merge pull request 'Web security configuration' (#15) from configurable-web-security into develop
All checks were successful
Lint / pnpm_install (push) Successful in 2m5s
Publish Docker image / Build (push) Successful in 5m48s
Test (production install and build) / production (20.16.0) (push) Successful in 1m22s
Test (backend) / unit (20.16.0) (push) Successful in 8m44s
Lint / lint (backend) (push) Successful in 2m31s
Lint / lint (frontend-embed) (push) Successful in 2m28s
Lint / lint (frontend) (push) Successful in 2m57s
Test (backend) / e2e (20.16.0) (push) Successful in 12m23s
Lint / lint (frontend-shared) (push) Successful in 2m34s
Lint / lint (misskey-bubble-game) (push) Successful in 2m37s
Lint / lint (misskey-js) (push) Successful in 2m26s
Lint / lint (sw) (push) Successful in 2m34s
Lint / lint (misskey-reversi) (push) Successful in 2m51s
Lint / typecheck (backend) (push) Successful in 2m11s
Lint / typecheck (misskey-js) (push) Successful in 1m51s
Lint / typecheck (sw) (push) Successful in 2m1s
All checks were successful
Lint / pnpm_install (push) Successful in 2m5s
Publish Docker image / Build (push) Successful in 5m48s
Test (production install and build) / production (20.16.0) (push) Successful in 1m22s
Test (backend) / unit (20.16.0) (push) Successful in 8m44s
Lint / lint (backend) (push) Successful in 2m31s
Lint / lint (frontend-embed) (push) Successful in 2m28s
Lint / lint (frontend) (push) Successful in 2m57s
Test (backend) / e2e (20.16.0) (push) Successful in 12m23s
Lint / lint (frontend-shared) (push) Successful in 2m34s
Lint / lint (misskey-bubble-game) (push) Successful in 2m37s
Lint / lint (misskey-js) (push) Successful in 2m26s
Lint / lint (sw) (push) Successful in 2m34s
Lint / lint (misskey-reversi) (push) Successful in 2m51s
Lint / typecheck (backend) (push) Successful in 2m11s
Lint / typecheck (misskey-js) (push) Successful in 1m51s
Lint / typecheck (sw) (push) Successful in 2m1s
Reviewed-on: #15
This commit is contained in:
commit
073c70d42b
8 changed files with 154 additions and 33 deletions
|
@ -168,8 +168,8 @@ id: 'aidx'
|
|||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
# ┌──────────────┐
|
||||
#──┘ Web Security └──────────────────────────────────────
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
@ -180,6 +180,24 @@ id: 'aidx'
|
|||
# - https://hstspreload.org/
|
||||
#hstsPreload: false
|
||||
|
||||
# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks.
|
||||
# browserSandboxing:
|
||||
# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true.
|
||||
# strictOriginReferrer: true
|
||||
# csp:
|
||||
# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution.
|
||||
# disable: false
|
||||
# # Merge additional directives into the CSP header. The default is an empty object.
|
||||
# # You may want to list your CDN or other trusted domains here.
|
||||
# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added.
|
||||
# appendDirectives:
|
||||
# 'script-src':
|
||||
# - "'unsafe-eval'" # do not use this ... just an example
|
||||
# - 'https://example.com'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
|
|
|
@ -162,8 +162,8 @@ id: 'aidx'
|
|||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
# ┌──────────────┐
|
||||
#──┘ Web Security └──────────────────────────────────────
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
@ -174,6 +174,24 @@ id: 'aidx'
|
|||
# - https://hstspreload.org/
|
||||
#hstsPreload: false
|
||||
|
||||
# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks.
|
||||
# browserSandboxing:
|
||||
# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true.
|
||||
# strictOriginReferrer: true
|
||||
# csp:
|
||||
# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution.
|
||||
# disable: false
|
||||
# # Merge additional directives into the CSP header. The default is an empty object.
|
||||
# # You may want to list your CDN or other trusted domains here.
|
||||
# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added.
|
||||
# appendDirectives:
|
||||
# 'script-src':
|
||||
# - "'unsafe-eval'" # do not use this ... just an example
|
||||
# - 'https://example.com'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
|
|
|
@ -244,8 +244,8 @@ id: 'aidx'
|
|||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
# ┌──────────────┐
|
||||
#──┘ Web Security └──────────────────────────────────────
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
@ -256,6 +256,24 @@ id: 'aidx'
|
|||
# - https://hstspreload.org/
|
||||
#hstsPreload: false
|
||||
|
||||
# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks.
|
||||
# browserSandboxing:
|
||||
# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true.
|
||||
# strictOriginReferrer: true
|
||||
# csp:
|
||||
# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution.
|
||||
# disable: false
|
||||
# # Merge additional directives into the CSP header. The default is an empty object.
|
||||
# # You may want to list your CDN or other trusted domains here.
|
||||
# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added.
|
||||
# appendDirectives:
|
||||
# 'script-src':
|
||||
# - "'unsafe-eval'" # do not use this ... just an example
|
||||
# - 'https://example.com'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
|
|
|
@ -20,6 +20,18 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
|||
prefix?: string;
|
||||
};
|
||||
|
||||
type BrowserSandboxing = {
|
||||
// send Referrer-Policy: strict-origin
|
||||
strictOriginReferrer?: boolean;
|
||||
csp?: {
|
||||
disable?: boolean;
|
||||
|
||||
appendDirectives?: {
|
||||
[directive: string]: string | string[];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 設定ファイルの型
|
||||
*/
|
||||
|
@ -65,6 +77,8 @@ type Source = {
|
|||
|
||||
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
|
||||
|
||||
browserSandboxing?: BrowserSandboxing;
|
||||
|
||||
setupPassword?: string;
|
||||
|
||||
proxy?: string;
|
||||
|
@ -155,7 +169,9 @@ export type Config = {
|
|||
proxyRemoteFiles: boolean | undefined;
|
||||
signToActivityPubGet: boolean | undefined;
|
||||
|
||||
cspPrerenderedContent: Map<string, CSPHashed>,
|
||||
browserSandboxing: BrowserSandboxing;
|
||||
|
||||
cspPrerenderedContent: Map<string, CSPHashed>;
|
||||
|
||||
version: string;
|
||||
gitDescribe: string;
|
||||
|
@ -252,6 +268,7 @@ export function loadConfig(): Config {
|
|||
version,
|
||||
gitCommit,
|
||||
gitDescribe,
|
||||
browserSandboxing: config.browserSandboxing ?? { strictOriginReferrer: true },
|
||||
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
|
||||
setupPassword: config.setupPassword,
|
||||
url: url.origin,
|
||||
|
|
|
@ -87,9 +87,9 @@ type UploadFromUrlArgs = {
|
|||
|
||||
@Injectable()
|
||||
export class DriveService {
|
||||
public static NoSuchFolderError = class extends Error {};
|
||||
public static InvalidFileNameError = class extends Error {};
|
||||
public static CannotUnmarkSensitiveError = class extends Error {};
|
||||
public static NoSuchFolderError = class extends Error { };
|
||||
public static InvalidFileNameError = class extends Error { };
|
||||
public static CannotUnmarkSensitiveError = class extends Error { };
|
||||
private registerLogger: Logger;
|
||||
private downloaderLogger: Logger;
|
||||
private deleteLogger: Logger;
|
||||
|
@ -170,11 +170,11 @@ export class DriveService {
|
|||
}
|
||||
|
||||
const baseUrl = this.meta.objectStorageBaseUrl
|
||||
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
|
||||
?? `${this.meta.objectStorageUseSSL ? 'https' : 'http'}://${this.meta.objectStorageEndpoint}${this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : ''}/${this.meta.objectStorageBucket}`;
|
||||
|
||||
// for original
|
||||
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||
const url = `${ baseUrl }/${ key }`;
|
||||
const url = `${baseUrl}/${key}`;
|
||||
|
||||
// for alts
|
||||
let webpublicKey: string | null = null;
|
||||
|
@ -191,7 +191,7 @@ export class DriveService {
|
|||
|
||||
if (alts.webpublic) {
|
||||
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||
webpublicUrl = `${baseUrl}/${webpublicKey}`;
|
||||
|
||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||
uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name));
|
||||
|
@ -199,7 +199,7 @@ export class DriveService {
|
|||
|
||||
if (alts.thumbnail) {
|
||||
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||
thumbnailUrl = `${baseUrl}/${thumbnailKey}`;
|
||||
|
||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`));
|
||||
|
|
|
@ -89,14 +89,28 @@ export class ServerService implements OnApplicationShutdown {
|
|||
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
||||
}
|
||||
|
||||
// Other Security/Privacy Headers
|
||||
fastify.addHook('onRequest', (_, reply, done) => {
|
||||
reply.header('x-content-type-options', 'nosniff');
|
||||
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
|
||||
if (this.config.browserSandboxing.strictOriginReferrer) {
|
||||
reply.header('referrer-policy', 'strict-origin');
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
// CSP
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.NODE_ENV === 'production' && !this.config.browserSandboxing.csp?.disable) {
|
||||
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
|
||||
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
|
||||
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined,
|
||||
script_src: [
|
||||
`https://${new URL(this.config.url).host}/embed_vite/`,
|
||||
`https://${new URL(this.config.url).host}/vite/`,
|
||||
],
|
||||
});
|
||||
fastify.addHook('onRequest', (_, reply, done) => {
|
||||
reply.header('Content-Security-Policy', generatedCSP);
|
||||
reply.header('content-security-policy', generatedCSP);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ export type CSPHashed = {
|
|||
|
||||
export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
|
||||
mediaProxy?: string,
|
||||
script_src?: string[],
|
||||
append?: { [key: string]: string | string[] },
|
||||
}) {
|
||||
const keys = Array.from(hashedMap.keys());
|
||||
const scripts = keys
|
||||
|
@ -22,7 +24,7 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
|
|||
.filter(name => name.endsWith('.css'))
|
||||
.map(name => `'${hashedMap.get(name)!.integrity}'`);
|
||||
|
||||
return ([
|
||||
const cpolicy = [
|
||||
['default-src', ['\'self\'']],
|
||||
['img-src',
|
||||
[
|
||||
|
@ -42,7 +44,11 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
|
|||
//
|
||||
// ref: https://github.com/shikijs/shiki/issues/671
|
||||
['style-src-attr', ['\'self\'', '\'unsafe-inline\'']],
|
||||
['script-src', ['\'self\'', '\'wasm-unsafe-eval\'', ...scripts]],
|
||||
['script-src', [
|
||||
...(options.script_src ? options.script_src : ['\'self\'']),
|
||||
'\'wasm-unsafe-eval\'',
|
||||
...scripts
|
||||
]],
|
||||
['object-src', ['\'none\'']],
|
||||
['base-uri', ['\'self\'']],
|
||||
['form-action', ['\'self\'']],
|
||||
|
@ -52,7 +58,23 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
|
|||
[
|
||||
['upgrade-insecure-requests', []],
|
||||
] : []),
|
||||
] as [string, string[]][])
|
||||
] 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('; ');
|
||||
|
|
|
@ -248,14 +248,28 @@ export class ClientServerService {
|
|||
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
||||
}
|
||||
|
||||
// Other Security/Privacy Headers
|
||||
fastify.addHook('onRequest', (_, reply, done) => {
|
||||
reply.header('x-content-type-options', 'nosniff');
|
||||
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
|
||||
if (this.config.browserSandboxing.strictOriginReferrer ?? true) {
|
||||
reply.header('referrer-policy', 'strict-origin');
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
// CSP
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
|
||||
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
|
||||
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined,
|
||||
script_src: [
|
||||
`https://${new URL(this.config.url).host}/embed_vite/`,
|
||||
`https://${new URL(this.config.url).host}/vite/`,
|
||||
],
|
||||
});
|
||||
fastify.addHook('onRequest', (_, reply, done) => {
|
||||
reply.header('Content-Security-Policy', generatedCSP);
|
||||
reply.header('content-security-policy', generatedCSP);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue