do not use media proxy if emoji is local

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-20 12:53:29 -06:00 committed by fly_mc
parent 7b622d797d
commit e2471b85dd
3 changed files with 56 additions and 6 deletions

View file

@ -11,15 +11,39 @@ import type { } from '@/models/Blocking.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { Config } from '@/config.js';
@Injectable() @Injectable()
export class EmojiEntityService { export class EmojiEntityService {
constructor( constructor(
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: 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 @bindThis
public packSimpleNoQuery( public packSimpleNoQuery(
emoji: MiEmoji, emoji: MiEmoji,
@ -29,7 +53,7 @@ export class EmojiEntityService {
name: emoji.name, name: emoji.name,
category: emoji.category, category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
localOnly: emoji.localOnly ? true : undefined, localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
@ -72,7 +96,7 @@ export class EmojiEntityService {
category: emoji.category, category: emoji.category,
host: emoji.host, host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
license: emoji.license, license: emoji.license,
isSensitive: emoji.isSensitive, isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly, localOnly: emoji.localOnly,

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * 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 { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
@ -98,3 +98,4 @@ export class MiFollowing {
public followeeSharedInbox: string | null; public followeeSharedInbox: string | null;
//#endregion //#endregion
} }

View file

@ -35,6 +35,11 @@ import { makeHstsHook } from './hsts.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url)); const _dirname = fileURLToPath(new URL('.', import.meta.url));
// 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() @Injectable()
export class ServerService implements OnApplicationShutdown { export class ServerService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
@ -139,7 +144,7 @@ export class ServerService implements OnApplicationShutdown {
name: name, 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 (emoji == null) {
if ('fallback' in request.query) { if ('fallback' in request.query) {
@ -150,16 +155,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; let url: URL;
if ('badge' in request.query) { if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`); url = new URL(`${this.config.mediaProxy}/emoji.png`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url.searchParams.set('url', dbUrl);
url.searchParams.set('badge', '1'); url.searchParams.set('badge', '1');
} else { } else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`); url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url.searchParams.set('url', dbUrl);
url.searchParams.set('emoji', '1'); url.searchParams.set('emoji', '1');
if ('static' in request.query) url.searchParams.set('static', '1'); if ('static' in request.query) url.searchParams.set('static', '1');
} }
@ -183,6 +198,16 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400'); reply.header('Cache-Control', 'public, max-age=86400');
if (user) { 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)); reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
} else { } else {
reply.redirect('/static-assets/user-unknown.png'); reply.redirect('/static-assets/user-unknown.png');