From 2be1a39d1375f50ce4dbdbb336936b17cd59445f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 7 Feb 2023 19:58:58 +0900
Subject: [PATCH] fix(server): validate urls from ap to improve security

---
 CHANGELOG.md                                    |  1 +
 .../core/activitypub/models/ApNoteService.ts    | 17 +++++++++++++----
 .../core/activitypub/models/ApPersonService.ts  | 16 ++++++++++++++--
 3 files changed, 28 insertions(+), 6 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c99e8f16b..1514de895e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ You should also include the user name that made the change.
 
 ### Bugfixes
 - Client: MkEmojiPickerでもChromeで検索ダイアログで変換確定するとそのまま検索されてしまうのを修正
+- fix(server): validate urls from ap to improve security
 
 ## 13.4.0 (2023/02/05)
 
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index c9192f53b7..813415e6f6 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -1,8 +1,7 @@
 import { forwardRef, Inject, Injectable } from '@nestjs/common';
 import promiseLimit from 'promise-limit';
 import { DI } from '@/di-symbols.js';
-import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js';
-import type { UsersRepository } from '@/models/index.js';
+import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
 import type { Config } from '@/config.js';
 import type { CacheableRemoteUser } from '@/models/entities/User.js';
 import type { Note } from '@/models/entities/Note.js';
@@ -18,6 +17,7 @@ import { PollService } from '@/core/PollService.js';
 import { StatusError } from '@/misc/status-error.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { MessagingService } from '@/core/MessagingService.js';
+import { bindThis } from '@/decorators.js';
 import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
 // eslint-disable-next-line @typescript-eslint/consistent-type-imports
 import { ApLoggerService } from '../ApLoggerService.js';
@@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js';
 import { ApImageService } from './ApImageService.js';
 import type { Resolver } from '../ApResolverService.js';
 import type { IObject, IPost } from '../type.js';
-import { bindThis } from '@/decorators.js';
 
 @Injectable()
 export class ApNoteService {
@@ -133,6 +132,16 @@ export class ApNoteService {
 		const note: IPost = object;
 	
 		this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
+
+		if (note.id && !note.id.startsWith('https://')) {
+			throw new Error('unexpected shcema of note.id: ' + note.id);
+		}
+
+		const url = getOneApHrefNullable(note.url);
+
+		if (url && !url.startsWith('https://')) {
+			throw new Error('unexpected shcema of note url: ' + url);
+		}
 	
 		this.logger.info(`Creating the Note: ${note.id}`);
 	
@@ -307,7 +316,7 @@ export class ApNoteService {
 			apEmojis,
 			poll,
 			uri: note.id,
-			url: getOneApHrefNullable(note.url),
+			url: url,
 		}, silent);
 	}
 	
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 2325bbe093..76f820cda0 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -252,6 +252,12 @@ export class ApPersonService implements OnModuleInit {
 
 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
 
+		const url = getOneApHrefNullable(person.url);
+
+		if (url && !url.startsWith('https://')) {
+			throw new Error('unexpected shcema of person url: ' + url);
+		}
+
 		// Create user
 		let user: IRemoteUser;
 		try {
@@ -283,7 +289,7 @@ export class ApPersonService implements OnModuleInit {
 				await transactionalEntityManager.save(new UserProfile({
 					userId: user.id,
 					description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
-					url: getOneApHrefNullable(person.url),
+					url: url,
 					fields,
 					birthday: bday ? bday[0] : null,
 					location: person['vcard:Address'] ?? null,
@@ -425,6 +431,12 @@ export class ApPersonService implements OnModuleInit {
 
 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
 
+		const url = getOneApHrefNullable(person.url);
+
+		if (url && !url.startsWith('https://')) {
+			throw new Error('unexpected shcema of person url: ' + url);
+		}
+
 		const updates = {
 			lastFetchedAt: new Date(),
 			inbox: person.inbox,
@@ -459,7 +471,7 @@ export class ApPersonService implements OnModuleInit {
 		}
 
 		await this.userProfilesRepository.update({ userId: exist.id }, {
-			url: getOneApHrefNullable(person.url),
+			url: url,
 			fields,
 			description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
 			birthday: bday ? bday[0] : null,
-- 
GitLab