diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 4904357d3..dae5d62d3 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -4,6 +4,7 @@ import { getUserKeypair } from '@/misc/keypair-store.js'; import { User } from '@/models/entities/user.js'; import { UserKeypair } from '@/models/entities/user-keypair.js'; import { getResponse } from '@/misc/fetch.js'; +import { getApId } from './type.js'; import { createSignedPost, createSignedGet } from './ap-request.js'; import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js'; @@ -128,7 +129,25 @@ export async function signedGet(_url: string, user: { id: User['id'] }): Promise if (!isActivitypub(res.headers.get('Content-Type'))) { throw new Error('invalid response content type'); } - return await res.json(); + + const data = await res.json(); + // In theory, activitypub allows for `id` to be null for ephemeral + // objects, but we wouldn't be fetching those with signed get, since + // they are... ephemeral. + const id = new URL(getApId(data)); + if (id.href !== url.href) { + // if the id and fetched url mismatch, treat it as if it was a redirect + // SECURITY: this is to prevent impersonation via improper media files + url = id; + // if this kind of "redirect" happens, there should be at most one more + // redirect since we now have the canonical url. setting to 2 because it + // will be decremented to 1 right away by the for loop. + if (redirects > 2) { + redirects = 2; + } + } else { + return data; + } } }