Web security configuration

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-12 02:18:25 -06:00
parent 7748ab5dd0
commit afac979977
No known key found for this signature in database
15 changed files with 163 additions and 37 deletions

View file

@ -168,8 +168,8 @@ id: 'aidx'
# options: # options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐ # ──────────────┐
#───┘ Other configuration └───────────────────────────────────── #──┘ Web Security └─────────────────────────────────────
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #disableHsts: true
@ -180,6 +180,24 @@ id: 'aidx'
# - https://hstspreload.org/ # - https://hstspreload.org/
#hstsPreload: false #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 # Number of worker processes
#clusterLimit: 1 #clusterLimit: 1

View file

@ -162,8 +162,8 @@ id: 'aidx'
# options: # options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐ # ──────────────┐
#───┘ Other configuration └───────────────────────────────────── #──┘ Web Security └─────────────────────────────────────
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #disableHsts: true
@ -174,6 +174,24 @@ id: 'aidx'
# - https://hstspreload.org/ # - https://hstspreload.org/
#hstsPreload: false #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 # Number of worker processes
#clusterLimit: 1 #clusterLimit: 1

View file

@ -244,8 +244,8 @@ id: 'aidx'
# options: # options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐ # ──────────────┐
#───┘ Other configuration └───────────────────────────────────── #──┘ Web Security └─────────────────────────────────────
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #disableHsts: true
@ -256,6 +256,24 @@ id: 'aidx'
# - https://hstspreload.org/ # - https://hstspreload.org/
#hstsPreload: false #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 # Number of worker processes
#clusterLimit: 1 #clusterLimit: 1

View file

@ -2119,6 +2119,7 @@ _permissions:
"read:flash-likes": "View list of liked Plays" "read:flash-likes": "View list of liked Plays"
"write:flash-likes": "Edit list of liked Plays" "write:flash-likes": "Edit list of liked Plays"
"read:admin:abuse-user-reports": "View user reports" "read:admin:abuse-user-reports": "View user reports"
"write:admin:create-account": "Create user account"
"write:admin:delete-account": "Delete user account" "write:admin:delete-account": "Delete user account"
"write:admin:delete-all-files-of-a-user": "Delete all files of a user" "write:admin:delete-all-files-of-a-user": "Delete all files of a user"
"read:admin:index-stats": "View database index stats" "read:admin:index-stats": "View database index stats"

View file

@ -2163,6 +2163,7 @@ _permissions:
"read:flash-likes": "Playのいいねを見る" "read:flash-likes": "Playのいいねを見る"
"write:flash-likes": "Playのいいねを操作する" "write:flash-likes": "Playのいいねを操作する"
"read:admin:abuse-user-reports": "ユーザーからの通報を見る" "read:admin:abuse-user-reports": "ユーザーからの通報を見る"
"write:admin:create-account": "ユーザーアカウントを作成する"
"write:admin:delete-account": "ユーザーアカウントを削除する" "write:admin:delete-account": "ユーザーアカウントを削除する"
"write:admin:delete-all-files-of-a-user": "ユーザーのすべてのファイルを削除する" "write:admin:delete-all-files-of-a-user": "ユーザーのすべてのファイルを削除する"
"read:admin:index-stats": "データベースインデックスに関する情報を見る" "read:admin:index-stats": "データベースインデックスに関する情報を見る"

View file

@ -20,6 +20,18 @@ type RedisOptionsSource = Partial<RedisOptions> & {
prefix?: string; 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; publishTarballInsteadOfProvideRepositoryUrl?: boolean;
browserSandboxing?: BrowserSandboxing;
setupPassword?: string; setupPassword?: string;
proxy?: string; proxy?: string;
@ -155,7 +169,9 @@ export type Config = {
proxyRemoteFiles: boolean | undefined; proxyRemoteFiles: boolean | undefined;
signToActivityPubGet: boolean | undefined; signToActivityPubGet: boolean | undefined;
cspPrerenderedContent: Map<string, CSPHashed>, browserSandboxing: BrowserSandboxing;
cspPrerenderedContent: Map<string, CSPHashed>;
version: string; version: string;
gitDescribe: string; gitDescribe: string;
@ -252,6 +268,7 @@ export function loadConfig(): Config {
version, version,
gitCommit, gitCommit,
gitDescribe, gitDescribe,
browserSandboxing: config.browserSandboxing ?? { strictOriginReferrer: true },
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
setupPassword: config.setupPassword, setupPassword: config.setupPassword,
url: url.origin, url: url.origin,

View file

@ -87,9 +87,9 @@ type UploadFromUrlArgs = {
@Injectable() @Injectable()
export class DriveService { export class DriveService {
public static NoSuchFolderError = class extends Error {}; public static NoSuchFolderError = class extends Error { };
public static InvalidFileNameError = class extends Error {}; public static InvalidFileNameError = class extends Error { };
public static CannotUnmarkSensitiveError = class extends Error {}; public static CannotUnmarkSensitiveError = class extends Error { };
private registerLogger: Logger; private registerLogger: Logger;
private downloaderLogger: Logger; private downloaderLogger: Logger;
private deleteLogger: Logger; private deleteLogger: Logger;
@ -170,11 +170,11 @@ export class DriveService {
} }
const baseUrl = this.meta.objectStorageBaseUrl 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 // for original
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`; const url = `${baseUrl}/${key}`;
// for alts // for alts
let webpublicKey: string | null = null; let webpublicKey: string | null = null;
@ -191,7 +191,7 @@ export class DriveService {
if (alts.webpublic) { if (alts.webpublic) {
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${baseUrl}/${webpublicKey}`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name)); 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) { if (alts.thumbnail) {
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${baseUrl}/${thumbnailKey}`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`)); uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`));

View file

@ -89,14 +89,28 @@ export class ServerService implements OnApplicationShutdown {
fastify.addHook('onRequest', makeHstsHook(host, preload)); 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 // CSP
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production' && !this.config.browserSandboxing.csp?.disable) {
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent); console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, { const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined, 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) => { fastify.addHook('onRequest', (_, reply, done) => {
reply.header('Content-Security-Policy', generatedCSP); reply.header('content-security-policy', generatedCSP);
done(); done();
}); });
} }

View file

@ -19,6 +19,8 @@ import { Packed } from '@/misc/json-schema.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
kind: 'write:admin:create-account',
errors: { errors: {
accessDenied: { accessDenied: {
message: 'Access denied.', message: 'Access denied.',

View file

@ -13,6 +13,8 @@ export type CSPHashed = {
export function generateCSP(hashedMap: Map<string, CSPHashed>, options: { export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
mediaProxy?: string, mediaProxy?: string,
script_src?: string[],
append?: { [key: string]: string | string[] },
}) { }) {
const keys = Array.from(hashedMap.keys()); const keys = Array.from(hashedMap.keys());
const scripts = keys const scripts = keys
@ -22,7 +24,7 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
.filter(name => name.endsWith('.css')) .filter(name => name.endsWith('.css'))
.map(name => `'${hashedMap.get(name)!.integrity}'`); .map(name => `'${hashedMap.get(name)!.integrity}'`);
return ([ const cpolicy = [
['default-src', ['\'self\'']], ['default-src', ['\'self\'']],
['img-src', ['img-src',
[ [
@ -42,7 +44,11 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
// //
// ref: https://github.com/shikijs/shiki/issues/671 // ref: https://github.com/shikijs/shiki/issues/671
['style-src-attr', ['\'self\'', '\'unsafe-inline\'']], ['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\'']], ['object-src', ['\'none\'']],
['base-uri', ['\'self\'']], ['base-uri', ['\'self\'']],
['form-action', ['\'self\'']], ['form-action', ['\'self\'']],
@ -52,7 +58,23 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
[ [
['upgrade-insecure-requests', []], ['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]) => { .map(([name, values]) => {
return `${name} ${values.join(' ')}`; return `${name} ${values.join(' ')}`;
}).join('; '); }).join('; ');

View file

@ -248,14 +248,28 @@ export class ClientServerService {
fastify.addHook('onRequest', makeHstsHook(host, preload)); 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 // CSP
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent); console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, { const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined, 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) => { fastify.addHook('onRequest', (_, reply, done) => {
reply.header('Content-Security-Policy', generatedCSP); reply.header('content-security-policy', generatedCSP);
done(); done();
}); });
} }

View file

@ -2876,7 +2876,7 @@ type PartialRolePolicyOverride = Partial<{
}>; }>;
// @public (undocumented) // @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:create-account", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented) // @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json']; type PingResponse = operations['ping']['responses']['200']['content']['application/json'];

View file

@ -88,7 +88,7 @@ declare module '../api.js' {
/** /**
* No description provided. * No description provided.
* *
* **Credential required**: *No* * **Credential required**: *No* / **Permission**: *write:admin:create-account*
*/ */
request<E extends 'admin/accounts/create', P extends Endpoints[E]['req']>( request<E extends 'admin/accounts/create', P extends Endpoints[E]['req']>(
endpoint: E, endpoint: E,

View file

@ -85,7 +85,7 @@ export type paths = {
* admin/accounts/create * admin/accounts/create
* @description No description provided. * @description No description provided.
* *
* **Credential required**: *No* * **Credential required**: *No* / **Permission**: *write:admin:create-account*
*/ */
post: operations['admin___accounts___create']; post: operations['admin___accounts___create'];
}; };
@ -5659,7 +5659,7 @@ export type operations = {
* admin/accounts/create * admin/accounts/create
* @description No description provided. * @description No description provided.
* *
* **Credential required**: *No* * **Credential required**: *No* / **Permission**: *write:admin:create-account*
*/ */
admin___accounts___create: { admin___accounts___create: {
requestBody: { requestBody: {

View file

@ -64,6 +64,7 @@ export const permissions = [
'read:flash-likes', 'read:flash-likes',
'write:flash-likes', 'write:flash-likes',
'read:admin:abuse-user-reports', 'read:admin:abuse-user-reports',
'write:admin:create-account',
'write:admin:delete-account', 'write:admin:delete-account',
'write:admin:delete-all-files-of-a-user', 'write:admin:delete-all-files-of-a-user',
'read:admin:index-stats', 'read:admin:index-stats',