From 7a7aef71cd6344c486d129789aff64f6e79a2824 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 20 Nov 2024 12:53:29 -0600 Subject: [PATCH] do not use media proxy if emoji is local Signed-off-by: eternal-flame-AD --- .../src/core/entities/EmojiEntityService.ts | 28 ++++++++++++++-- packages/backend/src/models/Following.ts | 3 +- packages/backend/src/server/ServerService.ts | 32 ++++++++++++++++--- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 8929d4e640..391d972320 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -11,15 +11,39 @@ import type { } from '@/models/Blocking.js'; import type { MiEmoji } from '@/models/Emoji.js'; import { bindThis } from '@/decorators.js'; import { In } from 'typeorm'; +import type { Config } from '@/config.js'; @Injectable() export class EmojiEntityService { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + + @Inject(DI.config) + private config: Config, ) { } + private stripProxyIfOrigin(url: string): string { + try { + const u = new URL(url); + let origin = u.origin; + if (u.origin === new URL(this.config.mediaProxy).origin) { + const innerUrl = u.searchParams.get('url'); + if (innerUrl) { + origin = new URL(innerUrl).origin; + } + } + if (origin === u.origin) { + return url; + } + } catch (e) { + return url; + } + + return url; + } + @bindThis public packSimpleNoQuery( emoji: MiEmoji, @@ -29,7 +53,7 @@ export class EmojiEntityService { name: emoji.name, category: emoji.category, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url: emoji.publicUrl || emoji.originalUrl, + url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl), localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, @@ -72,7 +96,7 @@ export class EmojiEntityService { category: emoji.category, host: emoji.host, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url: emoji.publicUrl || emoji.originalUrl, + url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl), license: emoji.license, isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts index 62cbc29f26..a64d1a4caf 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, ViewEntity } from 'typeorm'; import { id } from './util/id.js'; import { MiUser } from './User.js'; @@ -98,3 +98,4 @@ export class MiFollowing { public followeeSharedInbox: string | null; //#endregion } + diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index e49ea9432b..e55a52fcab 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -33,7 +33,6 @@ import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import { makeHstsHook } from './hsts.js'; import { generateCSP } from './csp.js'; -import * as prom from 'prom-client'; import { sanitizeRequestURI } from '@/misc/log-sanitization.js'; import { metricCounter, metricGauge, metricHistogram, MetricsService } from './api/MetricsService.js'; @@ -110,6 +109,11 @@ const mLastSuccessfulRequest = metricGauge({ labelNames: [], }); +// This function is used to determine if a path is safe to redirect to. +function redirectSafePath(path: string): boolean { + return ['/files/', '/identicon/', '/proxy/', '/static-assets/', '/vite/', '/embed_vite/'].some(prefix => path.startsWith(prefix)); +} + @Injectable() export class ServerService implements OnApplicationShutdown { private logger: Logger; @@ -348,7 +352,7 @@ export class ServerService implements OnApplicationShutdown { name: name, }); - reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + reply.header('Content-Security-Policy', 'default-src \'none\''); if (emoji == null) { if ('fallback' in request.query) { @@ -359,16 +363,26 @@ export class ServerService implements OnApplicationShutdown { } } + const dbUrl = emoji?.publicUrl || emoji?.originalUrl; + const dbUrlParsed = new URL(dbUrl); + const instanceUrl = new URL(this.config.url); + if (dbUrlParsed.origin === instanceUrl.origin) { + if (!redirectSafePath(dbUrlParsed.pathname)) { + return await reply.status(508); + } + return await reply.redirect(dbUrl, 301); + } + let url: URL; if ('badge' in request.query) { url = new URL(`${this.config.mediaProxy}/emoji.png`); // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); + url.searchParams.set('url', dbUrl); url.searchParams.set('badge', '1'); } else { url = new URL(`${this.config.mediaProxy}/emoji.webp`); // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); + url.searchParams.set('url', dbUrl); url.searchParams.set('emoji', '1'); if ('static' in request.query) url.searchParams.set('static', '1'); } @@ -392,6 +406,16 @@ export class ServerService implements OnApplicationShutdown { reply.header('Cache-Control', 'public, max-age=86400'); if (user) { + const dbUrl = user?.avatarUrl ?? this.userEntityService.getIdenticonUrl(user); + const dbUrlParsed = new URL(dbUrl); + const instanceUrl = new URL(this.config.url); + if (dbUrlParsed.origin === instanceUrl.origin) { + if (!redirectSafePath(dbUrlParsed.pathname)) { + return await reply.status(508); + } + return await reply.redirect(dbUrl, 301); + } + reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user)); } else { reply.redirect('/static-assets/user-unknown.png');