From e917dc6be368078055960e29750a97263ff0c39d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 4 Feb 2023 09:10:01 +0900
Subject: [PATCH 1/2] fix(client): validate urls to improve security

---
 packages/client/src/components/MkUrlPreview.vue | 1 +
 packages/client/src/components/global/MkUrl.vue | 1 +
 packages/client/src/pages/miauth.vue            | 2 ++
 3 files changed, 4 insertions(+)

diff --git a/packages/client/src/components/MkUrlPreview.vue b/packages/client/src/components/MkUrlPreview.vue
index ef65cb796..865d4bcbe 100644
--- a/packages/client/src/components/MkUrlPreview.vue
+++ b/packages/client/src/components/MkUrlPreview.vue
@@ -67,6 +67,7 @@ const embedId = `embed${Math.random().toString().replace(/\D/,'')}`;
 let tweetHeight = $ref(150);
 
 const requestUrl = new URL(props.url);
+if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
 
 if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com') {
 	const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
diff --git a/packages/client/src/components/global/MkUrl.vue b/packages/client/src/components/global/MkUrl.vue
index 2d328211d..d22c0b299 100644
--- a/packages/client/src/components/global/MkUrl.vue
+++ b/packages/client/src/components/global/MkUrl.vue
@@ -33,6 +33,7 @@ const props = defineProps<{
 
 const self = props.url.startsWith(local);
 const url = new URL(props.url);
+if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
 const el = ref();
 
 useTooltip(el, (showing) => {
diff --git a/packages/client/src/pages/miauth.vue b/packages/client/src/pages/miauth.vue
index 6352dc329..eaf96d60f 100644
--- a/packages/client/src/pages/miauth.vue
+++ b/packages/client/src/pages/miauth.vue
@@ -70,6 +70,8 @@ async function accept(): Promise<void> {
 
 	state = 'accepted';
 	if (props.callback) {
+		const cbUrl = new URL(props.callback);
+		if (!['http:', 'https:'].includes(cbUrl.protocol)) throw new Error('invalid url');
 		location.href = appendQuery(props.callback, query({
 			session: props.session,
 		}));

From 76011a3f28e38d0561efcc4ca1d3997554717950 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Fri, 10 Feb 2023 11:14:33 -0800
Subject: [PATCH 2/2] fix: :lock: prevent issues

---
 .../src/remote/activitypub/models/note.ts        | 12 +++++++++++-
 .../src/remote/activitypub/models/person.ts      | 16 ++++++++++++++--
 packages/backend/src/server/web/url-preview.ts   |  8 ++++++++
 packages/client/src/pages/auth.vue               |  2 ++
 packages/client/src/pages/miauth.vue             |  3 +--
 packages/client/src/scripts/aiscript/api.ts      |  6 +++++-
 6 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
index afb3af6cb..34d8d0ba1 100644
--- a/packages/backend/src/remote/activitypub/models/note.ts
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -111,6 +111,16 @@ export async function createNote(
 
 	const note: IPost = object;
 
+	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}`);
+	}
+
 	logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
 
 	logger.info(`Creating the Note: ${note.id}`);
@@ -345,7 +355,7 @@ export async function createNote(
 			apEmojis,
 			poll,
 			uri: note.id,
-			url: getOneApHrefNullable(note.url),
+			url: url,
 		},
 		silent,
 	);
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index 0ec671f0a..88bbca5c4 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -195,6 +195,12 @@ export async function createPerson(
 
 	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 {
@@ -237,7 +243,7 @@ export async function createPerson(
 					description: person.summary
 						? 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,
@@ -387,6 +393,12 @@ export async function updatePerson(
 
 	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,
@@ -430,7 +442,7 @@ export async function updatePerson(
 	await UserProfiles.update(
 		{ userId: exist.id },
 		{
-			url: getOneApHrefNullable(person.url),
+			url: url,
 			fields,
 			description: person.summary
 				? htmlToMfm(truncate(person.summary, summaryLength), person.tag)
diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts
index d7da4e72c..cb58efa81 100644
--- a/packages/backend/src/server/web/url-preview.ts
+++ b/packages/backend/src/server/web/url-preview.ts
@@ -44,6 +44,14 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => {
 
 		logger.succ(`Got preview of ${url}: ${summary.title}`);
 
+		if (summary.url && !(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 = wrap(summary.icon);
 		summary.thumbnail = wrap(summary.thumbnail);
 
diff --git a/packages/client/src/pages/auth.vue b/packages/client/src/pages/auth.vue
index bb3c54bd3..9fc04d4f4 100644
--- a/packages/client/src/pages/auth.vue
+++ b/packages/client/src/pages/auth.vue
@@ -80,6 +80,8 @@ export default defineComponent({
 			this.state = 'accepted';
 			const getUrlParams = () => window.location.search.substring(1).split('&').reduce((result, query) => { const [k, v] = query.split('='); result[k] = decodeURI(v); return result; }, {});
 			if (this.session.app.callbackUrl) {
+				const url = new URL(this.session.app.callbackUrl);
+				if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
 				location.href = `${this.session.app.callbackUrl}?token=${this.session.token}&code=${this.session.token}&state=${getUrlParams().state || ''}`;
 			}
 		}, onLogin(res) {
diff --git a/packages/client/src/pages/miauth.vue b/packages/client/src/pages/miauth.vue
index eaf96d60f..292b47338 100644
--- a/packages/client/src/pages/miauth.vue
+++ b/packages/client/src/pages/miauth.vue
@@ -71,14 +71,13 @@ async function accept(): Promise<void> {
 	state = 'accepted';
 	if (props.callback) {
 		const cbUrl = new URL(props.callback);
-		if (!['http:', 'https:'].includes(cbUrl.protocol)) throw new Error('invalid url');
+		if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
 		location.href = appendQuery(props.callback, query({
 			session: props.session,
 		}));
 	}
 }
 
-function deny(): void {
 	state = 'denied';
 }
 
diff --git a/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts
index b37eca8ab..32560b4ab 100644
--- a/packages/client/src/scripts/aiscript/api.ts
+++ b/packages/client/src/scripts/aiscript/api.ts
@@ -24,7 +24,11 @@ export function createAiScriptEnv(opts) {
 			return confirm.canceled ? values.FALSE : values.TRUE;
 		}),
 		"Mk:api": values.FN_NATIVE(async ([ep, param, token]) => {
-			if (token) utils.assertString(token);
+			if (token) {
+				utils.assertString(token);
+				// バグがあればundefinedもあり得るため念のため
+				if (typeof token.value !== 'string') throw new Error('invalid token');
+			}
 			apiRequests++;
 			if (apiRequests > 16) return values.NULL;
 			const res = await os.api(