diff --git a/CHANGELOG.md b/CHANGELOG.md index fd56700e1f..dff80b8e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## 2024.11.1 +### Note +- 登録時メールアドレスドメインのブラックリストの判定方式が後方一致から完全一致に変更されました。サブドメインごとブロックする場合は先頭に`.`を付けることで後方一致になります。 + - 例: `example.com` は `example.com` をブロックしますが、`sub.example.com` をブロックしません。 + - 例: `.example.com` は `example.com` および `sub.example.com` をブロックします。 + ### General - @@ -18,7 +23,9 @@ - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) - +- Enhance: 登録時メールアドレスドメインのブラックリストの判定方式を改善 +- Enhance: 登録時メールアドレスドメインのホワイトリストを追加 +- Enhance: Active Email Validationでブロックされたドメインを自動でブラックリストに追加するオプションを追加 ## 2024.11.0 diff --git a/packages/backend/migration/1732451011177-AddAllowedEmailDomains.js b/packages/backend/migration/1732451011177-AddAllowedEmailDomains.js new file mode 100644 index 0000000000..fd7e81ea9a --- /dev/null +++ b/packages/backend/migration/1732451011177-AddAllowedEmailDomains.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddAllowedEmailDomains1732451011177 { + name = 'AddAllowedEmailDomains1732451011177' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "allowedEmailDomains" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowedEmailDomains"`); + } +} diff --git a/packages/backend/migration/1732535648378-AddAutoAddBannedEmailDomain.js b/packages/backend/migration/1732535648378-AddAutoAddBannedEmailDomain.js new file mode 100644 index 0000000000..d0744b3e3f --- /dev/null +++ b/packages/backend/migration/1732535648378-AddAutoAddBannedEmailDomain.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddAutoAddBannedEmailDomain1732535648378 { + name = 'AddAutoAddBannedEmailDomain1732535648378' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableAutoAddBannedEmailDomain" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableAutoAddBannedEmailDomain"`); + } +} diff --git a/packages/backend/migration/1732538997055-AddEnableAllowedEmailDomainOnly.js b/packages/backend/migration/1732538997055-AddEnableAllowedEmailDomainOnly.js new file mode 100644 index 0000000000..4f30ac2dc8 --- /dev/null +++ b/packages/backend/migration/1732538997055-AddEnableAllowedEmailDomainOnly.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddEnableAllowedEmailDomainOnly1732538997055 { + name = 'AddEnableAllowedEmailDomainOnly1732538997055' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableAllowedEmailDomainsOnly" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableAllowedEmailDomainsOnly"`); + } +} diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index da198d0e42..75c65acbf3 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -16,6 +16,7 @@ import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class EmailService { @@ -34,6 +35,7 @@ export class EmailService { private loggerService: LoggerService, private utilityService: UtilityService, private httpRequestService: HttpRequestService, + private metaService: MetaService, ) { this.logger = this.loggerService.getLogger('email'); } @@ -162,8 +164,15 @@ export class EmailService { @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist' | 'allowedOnly'; }> { + if (!this.utilityService.validateEmailFormat(emailAddress)) { + return { + available: false, + reason: 'format', + }; + } + const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, @@ -176,6 +185,33 @@ export class EmailService { }; } + const emailDomain: string = emailAddress.split('@')[1]; + + // ホワイトリストに含まれている場合は即座にtrueを返す + if (this.utilityService.isAllowedHost(this.meta.allowedEmailDomains, emailDomain)) { + return { + available: true, + reason: null, + }; + } + + // ホワイトリストのみ許可の場合は即座にfalseを返す + if (this.meta.enableAllowedEmailDomainsOnly) { + return { + available: false, + reason: 'allowedOnly', + }; + } + + const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain); + + if (isBanned) { + return { + available: false, + reason: 'banned', + }; + } + let validated: { valid: boolean, reason?: string | null, @@ -208,22 +244,17 @@ export class EmailService { blacklist: 'blacklist', }; + // 自動追加が有効な場合はブラックリストに追加する + if (this.meta.enableAutoAddBannedEmailDomain) { + await this.addBlockedHost(emailDomain); + } + return { available: false, reason: validated.reason ? formatReason[validated.reason] ?? null : null, }; } - const emailDomain: string = emailAddress.split('@')[1]; - const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain); - - if (isBanned) { - return { - available: false, - reason: 'banned', - }; - } - return { available: true, reason: null, @@ -363,4 +394,11 @@ export class EmailService { }; } } + + private async addBlockedHost(domain: string) { + const set = {} as Partial; + set.bannedEmailDomains = this.meta.bannedEmailDomains; + set.bannedEmailDomains.push(domain); + await this.metaService.update(set); + } } diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 9a2ba72ed3..4ba6e4c7cb 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -39,10 +39,30 @@ export class UtilityService { return this.punyHost(uri) === this.toPuny(this.config.host); } + // メールアドレスのバリデーションを行う + // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + @bindThis + public validateEmailFormat(email: string): boolean { + const regexp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + return regexp.test(email); + } + @bindThis public isBlockedHost(blockedHosts: string[], host: string | null): boolean { if (host == null) return false; - return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); + return blockedHosts.some(x => { + if (x.startsWith('.')) return `.${host.toLowerCase()}`.endsWith(x); + return host.toLowerCase() === x; + }); + } + + @bindThis + public isAllowedHost(allowedHosts: string[], host: string | null): boolean { + if (host == null) return false; + return allowedHosts.some(x => { + if (x.startsWith('.')) return `.${host.toLowerCase()}`.endsWith(x); + return host.toLowerCase() === x; + }); } @bindThis diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index ad5e31ad6f..e386353497 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -569,6 +569,23 @@ export class MiMeta { }) public bannedEmailDomains: string[]; + @Column('varchar', { + length: 1024, + array: true, + default: '{}', + }) + public allowedEmailDomains: string[]; + + @Column('boolean', { + default: false, + }) + public enableAutoAddBannedEmailDomain: boolean; + + @Column('boolean', { + default: false, + }) + public enableAllowedEmailDomainsOnly: boolean; + @Column('varchar', { length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', }) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 64e3cc33bd..7429171a15 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -192,6 +192,14 @@ export const meta = { optional: false, nullable: false, }, }, + allowedEmailDomains: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, preservedUsernames: { type: 'array', optional: false, nullable: false, @@ -340,6 +348,14 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableAutoAddBannedEmailDomain: { + type: 'boolean', + optional: false, nullable: false, + }, + enableAllowedEmailDomainsOnly: { + type: 'boolean', + optional: false, nullable: false, + }, enableChartsForRemoteUser: { type: 'boolean', optional: false, nullable: false, @@ -637,12 +653,15 @@ export default class extends Endpoint { // eslint- enableTruemailApi: instance.enableTruemailApi, truemailInstance: instance.truemailInstance, truemailAuthKey: instance.truemailAuthKey, + enableAutoAddBannedEmailDomain: instance.enableAutoAddBannedEmailDomain, + enableAllowedEmailDomainsOnly: instance.enableAllowedEmailDomainsOnly, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances, enableServerMachineStats: instance.enableServerMachineStats, enableIdenticonGeneration: instance.enableIdenticonGeneration, bannedEmailDomains: instance.bannedEmailDomains, + allowedEmailDomains: instance.allowedEmailDomains, policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, enableFanoutTimeline: instance.enableFanoutTimeline, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 38ef0d1de8..e54096b6d5 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -141,6 +141,9 @@ export const paramDef = { enableIdenticonGeneration: { type: 'boolean' }, serverRules: { type: 'array', items: { type: 'string' } }, bannedEmailDomains: { type: 'array', items: { type: 'string' } }, + allowedEmailDomains: { type: 'array', items: { type: 'string' } }, + enableAutoAddBannedEmailDomain: { type: 'boolean' }, + enableAllowedEmailDomainsOnly: { type: 'boolean' }, preservedUsernames: { type: 'array', items: { type: 'string' } }, manifestJsonOverride: { type: 'string' }, enableFanoutTimeline: { type: 'boolean' }, @@ -639,6 +642,18 @@ export default class extends Endpoint { // eslint- set.bannedEmailDomains = ps.bannedEmailDomains; } + if (ps.allowedEmailDomains !== undefined) { + set.allowedEmailDomains = ps.allowedEmailDomains; + } + + if (ps.enableAutoAddBannedEmailDomain !== undefined) { + set.enableAutoAddBannedEmailDomain = ps.enableAutoAddBannedEmailDomain; + } + + if (ps.enableAllowedEmailDomainsOnly !== undefined) { + set.enableAllowedEmailDomainsOnly = ps.enableAllowedEmailDomainsOnly; + } + if (ps.urlPreviewEnabled !== undefined) { set.urlPreviewEnabled = ps.urlPreviewEnabled; } diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 975a4a1265..182b77f260 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -85,6 +85,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + + +
+ + + + + + +
@@ -181,6 +201,7 @@ const emailValidationForm = useForm({ enableTruemailApi: meta.enableTruemailApi, truemailInstance: meta.truemailInstance, truemailAuthKey: meta.truemailAuthKey, + enableAutoAddBannedEmailDomain: meta.enableAutoAddBannedEmailDomain, }, async (state) => { await os.apiWithDialog('admin/update-meta', { enableActiveEmailValidation: state.enableActiveEmailValidation, @@ -189,6 +210,18 @@ const emailValidationForm = useForm({ enableTruemailApi: state.enableTruemailApi, truemailInstance: state.truemailInstance, truemailAuthKey: state.truemailAuthKey, + enableAutoAddBannedEmailDomain: state.enableAutoAddBannedEmailDomain, + }); + fetchInstance(true); +}); + +const allowedEmailDomainsForm = useForm({ + allowedEmailDomains: meta.allowedEmailDomains?.join('\n') || '', + enableAllowedEmailDomainsOnly: meta.enableAllowedEmailDomainsOnly, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + allowedEmailDomains: state.allowedEmailDomains.split('\n'), + enableAllowedEmailDomainsOnly: state.enableAllowedEmailDomainsOnly, }); fetchInstance(true); }); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 42ca05e057..12b9ffb573 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5138,6 +5138,7 @@ export type operations = { prohibitedWords: string[]; prohibitedWordsForNameOfUser: string[]; bannedEmailDomains?: string[]; + allowedEmailDomains?: string[]; preservedUsernames: string[]; hcaptchaSecretKey: string | null; mcaptchaSecretKey: string | null; @@ -5175,6 +5176,8 @@ export type operations = { enableTruemailApi: boolean; truemailInstance: string | null; truemailAuthKey: string | null; + enableAutoAddBannedEmailDomain: boolean; + enableAllowedEmailDomainsOnly: boolean; enableChartsForRemoteUser: boolean; enableChartsForFederatedInstances: boolean; enableStatsForFederatedInstances: boolean; @@ -9578,6 +9581,9 @@ export type operations = { enableIdenticonGeneration?: boolean; serverRules?: string[]; bannedEmailDomains?: string[]; + allowedEmailDomains?: string[]; + enableAutoAddBannedEmailDomain?: boolean; + enableAllowedEmailDomainsOnly?: boolean; preservedUsernames?: string[]; manifestJsonOverride?: string; enableFanoutTimeline?: boolean;