diff --git a/CHANGELOG.md b/CHANGELOG.md index e6216ecf43..8529b2c522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2025.2.0-yumechinokuni.1 + +- Security: Revert miskey-dev/misskey#14897 +- Security: AP請求を外部ドメーンにリダイレクトしないように + ## 2025.2.0 ### General @@ -208,12 +213,6 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712) - Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) -- Fix: User Webhookテスト機能のMock Payloadを修正 -- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996) -- Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正 -- Fix: Inboxの処理で生じるエラーを誤ってActivityとして処理することがある問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/730) -- Fix: セキュリティに関する修正 ### Misskey.js - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 diff --git a/package.json b/package.json index 0984b0cc1a..76459b8c75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.2.0-yumechinokuni.0", + "version": "2025.2.0-yumechinokuni.1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts index 5610cfdeb8..ceb1caf6ee 100644 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ b/packages/backend/src/core/activitypub/misc/check-against-url.ts @@ -17,16 +17,16 @@ function getHrefFrom(one: IObject|string): string | undefined { } export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { - const idBad = activity.id !== undefined && !urls.includes(activity.id); + let idBad = activity.id !== undefined && !urls.includes(activity.id); // technically `activity.url` could be an `ApObject = IObject | // string | (IObject | string)[]`, but if it's a complicated thing // and the `activity.id` doesn't match, I think we're fine // rejecting the activity - const urlBad = typeof activity.url === 'string' && !urls.includes(activity.url); + const urlBad = activity.url != undefined && (typeof activity.url !== 'string' || !urls.includes(activity.url)); + // both of them have to pass checks, if present if (idBad || urlBad) { - const hosts = urls.map(u => { try { return new URL(u).host; @@ -42,6 +42,7 @@ export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${JSON.stringify(activity?.url)}) match location(${urls})`); } + // at least one of the key field must be present if (activity.id || activity.url) { return; } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 8c4b13a40a..71a69d4c33 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -29,7 +29,6 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { IActivity } from '@/core/activitypub/type.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; -import * as Acct from '@/misc/acct.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; @@ -487,16 +486,6 @@ export class ActivityPubServerService { return; } - // リモートだったらリダイレクト - if (user.host != null) { - if (user.uri == null || this.utilityService.isSelfHost(user.host)) { - reply.code(500); - return; - } - reply.redirect(user.uri, 301); - return; - } - reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); @@ -665,20 +654,19 @@ export class ActivityPubServerService { const user = await this.usersRepository.findOneBy({ id: userId, + host: IsNull(), isSuspended: false, }); return await this.userInfo(request, reply, user); }); - fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { + fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { vary(reply.raw, 'Accept'); - const acct = Acct.parse(request.params.acct); - const user = await this.usersRepository.findOneBy({ - usernameLower: acct.username, - host: acct.host ?? IsNull(), + usernameLower: request.params.user.toLowerCase(), + host: IsNull(), isSuspended: false, }); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 818efd8740..d1b8e9e05a 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -290,6 +290,35 @@ export class ServerService implements OnApplicationShutdown { done(); }); + fastify.addHook('onSend', (request, reply, payload, done) => { + if (reply.statusCode >= 300 && reply.statusCode < 400) { + const isAp = ["application/activity+json", "application/ld+json"].some(type => request.headers.accept?.includes(type)); + + if (isAp) { + const location = reply.getHeader('location'); + + // the only acceptable redirect is to our own domain + if (typeof location === 'string') { + // allow http in development + const normalizedLocation = process.env.NODE_ENV !== 'production' ? + location.replace(/^http:\/\//, 'https://') : location; + + if ([`https://${this.config.host}/`, `https://${this.config.hostname}/`].some(host => normalizedLocation.startsWith(host))) { + done(null, payload); + return; + } + } + + reply.code(406); + reply.removeHeader('location'); + done(null, null); + return; + } + } + + done(null, payload); + }); + // CSP if (process.env.NODE_ENV === 'production' && !this.config.browserSandboxing.csp?.disable) { console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 8ea4cb9800..7efd688ec2 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -230,7 +230,6 @@ describe('Webリソース', () => { path: path('xxxxxxxxxx'), type: HTML, })); - test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)'); }); describe.each([ @@ -250,7 +249,6 @@ describe('Webリソース', () => { path: path('xxxxxxxxxx'), accept, })); - test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)'); }); }); diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index ceccbcd532..ae59679912 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.2.0-yumechinokuni.0", + "version": "2025.2.0-yumechinokuni.1", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js",