From f3a4434830ac4cc2d12f814f880d41ba7b81b87b Mon Sep 17 00:00:00 2001 From: 鴇峰 朔華 <160555157+sakuhanight@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:41:33 +0900 Subject: fix(backend): メールアドレスの形式が正しくなければ以降の処理を行わないように (#15320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mod: バリデーションを追加 * 条件の修正 notつけわすれ * Update CHANGELOG.md --- packages/backend/src/core/UtilityService.ts | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'packages/backend/src/core/UtilityService.ts') diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index fcb750d3bf..23fb928ac9 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -38,6 +38,14 @@ 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; -- cgit v1.2.3-freya From 3455223fecd7eb4bec78c031bb80d4cfa0942945 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 1 Apr 2025 12:20:49 -0400 Subject: replace email validation regex with a simpler alternative --- packages/backend/src/core/UtilityService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/core/UtilityService.ts') diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index cb534a229c..5209cf7a3d 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -43,7 +43,9 @@ export class UtilityService { // 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])?)*$/; + // Note: replaced MK's complicated regex with a simpler one that is more efficient and reliable. + const regexp = /^.+@.+$/; + //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); } -- cgit v1.2.3-freya From fda71c414727b98ed52eb6a9195b3bbdcfda9054 Mon Sep 17 00:00:00 2001 From: dakkar Date: Mon, 21 Apr 2025 14:58:22 +0100 Subject: make `toPuny` work better in testing --- packages/backend/src/core/UtilityService.ts | 13 +++++++++++-- packages/backend/test/unit/UtilityService.ts | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/core/UtilityService.ts') diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 81eaa5f95d..9ed8138ffe 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -106,13 +106,22 @@ export class UtilityService { @bindThis public toPuny(host: string): string { - return domainToASCII(host.toLowerCase()); + // domainToASCII will return an empty string if we give it a + // string like `name:123`, but `host` may well be in that form + // (e.g. when testing locally, you'll get `localhost:3000`); split + // the port off, and add it back later + const hostParts = host.toLowerCase().match(/^(.+?)(:.+)?$/); + if (!hostParts) return ''; + const hostname = hostParts[1]; + const port = hostParts[2] ?? ''; + + return domainToASCII(hostname) + port; } @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; - return domainToASCII(host.toLowerCase()); + return this.toPuny(host); } @bindThis diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts index d86e794f2f..cb010ff1f9 100644 --- a/packages/backend/test/unit/UtilityService.ts +++ b/packages/backend/test/unit/UtilityService.ts @@ -22,6 +22,12 @@ describe('UtilityService', () => { test('japanese', () => { assert.equal(utilityService.punyHost('http://www.新聞.com'), 'www.xn--efvv70d.com'); }); + test('simple, with port', () => { + assert.equal(utilityService.punyHost('http://www.foo.com:3000'), 'www.foo.com:3000'); + }); + test('japanese, with port', () => { + assert.equal(utilityService.punyHost('http://www.新聞.com:3000'), 'www.xn--efvv70d.com:3000'); + }); }); describe('punyHostPSLDomain', () => { @@ -31,6 +37,12 @@ describe('UtilityService', () => { test('japanese', () => { assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com'), 'xn--efvv70d.com'); }); + test('simple, with port', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.foo.com:3000'), 'foo.com:3000'); + }); + test('japanese, with port', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com:3000'), 'xn--efvv70d.com:3000'); + }); test('lower', () => { assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'), 'foo.github.io'); assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'), 'bar.github.io'); @@ -40,4 +52,13 @@ describe('UtilityService', () => { assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.masto.host'), 'bar.masto.host'); }); }); + + describe('toPuny', () => { + test('without port ', () => { + assert.equal(utilityService.toPuny('www.foo.com'), 'www.foo.com'); + }); + test('with port ', () => { + assert.equal(utilityService.toPuny('www.foo.com:3000'), 'www.foo.com:3000'); + }); + }); }); -- cgit v1.2.3-freya From d6c2140821a4595862e063949d2f92530bd16cfd Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 5 May 2025 09:43:40 -0400 Subject: validate more URLs in UrlPreviewService.ts --- packages/backend/src/core/UtilityService.ts | 10 +++++ .../backend/src/server/web/UrlPreviewService.ts | 45 ++++++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) (limited to 'packages/backend/src/core/UtilityService.ts') diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index f8d04c0592..170afc72dc 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -176,4 +176,14 @@ export class UtilityService { const host = this.extractDbHost(uri); return this.isFederationAllowedHost(host); } + + @bindThis + public getUrlScheme(url: string): string { + try { + // Returns in the format "https:" or an empty string + return new URL(url).protocol; + } catch { + return ''; + } + } } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index d6151b665a..fe03583270 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -140,6 +140,8 @@ export class UrlPreviewService { ? await this.fetchSummaryFromProxy(url, this.meta, lang) : await this.fetchSummary(url, this.meta, lang); + this.validateUrls(summary); + // Repeat check, since redirects are allowed. if (this.utilityService.isBlockedHost(this.meta.blockedHosts, new URL(summary.url).host)) { reply.code(403); @@ -154,14 +156,6 @@ export class UrlPreviewService { this.logger.info(`Got preview of ${url} in ${lang}: ${summary.title}`); - if (!(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { - throw new Error('unsupported schema included'); - } - - if (summary.player.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { - throw new Error('unsupported schema included'); - } - summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); @@ -228,6 +222,41 @@ export class UrlPreviewService { return this.httpRequestService.getJson(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true); } + private validateUrls(summary: LocalSummalyResult) { + const urlScheme = this.utilityService.getUrlScheme(summary.url); + if (urlScheme !== 'http:' && urlScheme !== 'https:') { + throw new Error(`unsupported scheme in preview URL: "${urlScheme}"`); + } + + if (summary.player.url) { + const playerScheme = this.utilityService.getUrlScheme(summary.player.url); + if (playerScheme !== 'http:' && playerScheme !== 'https:') { + throw new Error(`unsupported scheme in player URL: "${playerScheme}"`); + } + } + + if (summary.icon) { + const iconScheme = this.utilityService.getUrlScheme(summary.icon); + if (iconScheme !== 'http:' && iconScheme !== 'https:') { + throw new Error(`unsupported scheme in icon URL: "${iconScheme}"`); + } + } + + if (summary.thumbnail) { + const thumbnailScheme = this.utilityService.getUrlScheme(summary.thumbnail); + if (thumbnailScheme !== 'http:' && thumbnailScheme !== 'https:') { + throw new Error(`unsupported scheme in thumbnail URL: "${thumbnailScheme}"`); + } + } + + if (summary.activityPub) { + const activityPubScheme = this.utilityService.getUrlScheme(summary.activityPub); + if (activityPubScheme !== 'http:' && activityPubScheme !== 'https:') { + throw new Error(`unsupported scheme in ActivityPub URL: "${activityPubScheme}"`); + } + } + } + private async inferActivityPubLink(summary: LocalSummalyResult) { // Match canonical URI first. // This covers local and remote links. -- cgit v1.2.3-freya