Web security configuration
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
7748ab5dd0
commit
afac979977
15 changed files with 163 additions and 37 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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": "データベースインデックスに関する情報を見る"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -147,11 +147,11 @@ export class DriveService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<MiDriveFile> {
|
private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<MiDriveFile> {
|
||||||
// thunbnail, webpublic を必要なら生成
|
// thunbnail, webpublic を必要なら生成
|
||||||
const alts = await this.generateAlts(path, type, !file.uri);
|
const alts = await this.generateAlts(path, type, !file.uri);
|
||||||
|
|
||||||
if (this.meta.useObjectStorage) {
|
if (this.meta.useObjectStorage) {
|
||||||
//#region ObjectStorage params
|
//#region ObjectStorage params
|
||||||
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
|
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
|
||||||
|
|
||||||
if (ext === '') {
|
if (ext === '') {
|
||||||
|
@ -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`));
|
||||||
|
@ -311,9 +311,9 @@ export class DriveService {
|
||||||
satisfyWebpublic = !!(
|
satisfyWebpublic = !!(
|
||||||
type !== 'image/svg+xml' && // security reason
|
type !== 'image/svg+xml' && // security reason
|
||||||
type !== 'image/avif' && // not supported by Mastodon and MS Edge
|
type !== 'image/avif' && // not supported by Mastodon and MS Edge
|
||||||
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
|
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
|
||||||
metadata.width && metadata.width <= 2048 &&
|
metadata.width && metadata.width <= 2048 &&
|
||||||
metadata.height && metadata.height <= 2048
|
metadata.height && metadata.height <= 2048
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.registerLogger.warn(`sharp failed: ${err}`);
|
this.registerLogger.warn(`sharp failed: ${err}`);
|
||||||
|
@ -470,11 +470,11 @@ export class DriveService {
|
||||||
const info = await this.fileInfoService.getFileInfo(path, {
|
const info = await this.fileInfoService.getFileInfo(path, {
|
||||||
skipSensitiveDetection: skipNsfwCheck,
|
skipSensitiveDetection: skipNsfwCheck,
|
||||||
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||||
0.5,
|
0.5,
|
||||||
sensitiveThresholdForPorn: 0.75,
|
sensitiveThresholdForPorn: 0.75,
|
||||||
enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
|
enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
|
||||||
});
|
});
|
||||||
|
@ -494,7 +494,7 @@ export class DriveService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user && !force) {
|
if (user && !force) {
|
||||||
// Check if there is a file with the same hash
|
// Check if there is a file with the same hash
|
||||||
const matched = await this.driveFilesRepository.findOneBy({
|
const matched = await this.driveFilesRepository.findOneBy({
|
||||||
md5: info.md5,
|
md5: info.md5,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -582,7 +582,7 @@ export class DriveService {
|
||||||
file.maybePorn = info.porn;
|
file.maybePorn = info.porn;
|
||||||
file.isSensitive = user
|
file.isSensitive = user
|
||||||
? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
||||||
sensitive ?? false
|
sensitive ?? false
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
||||||
|
@ -616,7 +616,7 @@ export class DriveService {
|
||||||
|
|
||||||
file = await this.driveFilesRepository.insertOne(file);
|
file = await this.driveFilesRepository.insertOne(file);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// duplicate key error (when already registered)
|
// duplicate key error (when already registered)
|
||||||
if (isDuplicateKeyValueError(err)) {
|
if (isDuplicateKeyValueError(err)) {
|
||||||
this.registerLogger.info(`already registered ${file.uri}`);
|
this.registerLogger.info(`already registered ${file.uri}`);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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('; ');
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue