From 96e1a93216eab6e42da37411becc9d997aaddb05 Mon Sep 17 00:00:00 2001 From: syuilo 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 ef65cb7960..865d4bcbe6 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 2d328211d5..d22c0b2999 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 6352dc329e..eaf96d60fb 100644 --- a/packages/client/src/pages/miauth.vue +++ b/packages/client/src/pages/miauth.vue @@ -70,6 +70,8 @@ async function accept(): Promise { 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 f7564d87b0b8ad50d6ba462feeac9e96725c71e1 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator 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 afb3af6cbc..34d8d0ba16 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 0ec671f0a1..88bbca5c46 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 d7da4e72ce..cb58efa812 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 bb3c54bd32..9fc04d4f49 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 eaf96d60fb..292b47338e 100644 --- a/packages/client/src/pages/miauth.vue +++ b/packages/client/src/pages/miauth.vue @@ -71,14 +71,13 @@ async function accept(): Promise { 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 b37eca8ab5..32560b4ab4 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(