mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-12-21 14:24:27 +00:00
* enhance: Add a few validation fixes from Sharkey See the original MR on the GitLab instance: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/484 Co-Authored-By: Dakkar <dakkar@thenautilus.net> * fix: primitive 2: acceptance of cross-origin alternate Co-Authored-By: Laura Hausmann <laura@hausmann.dev> * fix: primitive 3: validation of non-final url * fix: primitive 4: missing same-origin identifier validation of collection-wrapped activities * fix: primitives 5 & 8: reject activities with non string identifiers Co-Authored-By: Laura Hausmann <laura@hausmann.dev> * fix: primitive 6: reject anonymous objects that were fetched by their id * fix: primitives 9, 10 & 11: http signature validation doesn't enforce required headers or specify auth header name Co-Authored-By: Laura Hausmann <laura@hausmann.dev> * fix: primitive 14: improper validation of outbox, followers, following & shared inbox collections * fix: code style for primitive 14 * fix: primitive 15: improper same-origin validation for note uri and url Co-Authored-By: Laura Hausmann <laura@hausmann.dev> * fix: primitive 16: improper same-origin validation for user uri and url * fix: primitive 17: note same-origin identifier validation can be bypassed by wrapping the id in an array * fix: code style for primitive 17 * fix: check attribution against actor in notes While this isn't strictly required to fix the exploits at hand, this mirrors the fix in `ApQuestionService` for GHSA-5h8r-gq97-xv69, as a preemptive countermeasure. * fix: primitive 18: `ap/get` bypasses access checks One might argue that we could make this one actually preform access checks against the returned activity object, but I feel like that's a lot more work than just restricting it to administrators, since, to me at least, it seems more like a debugging tool than anything else. * fix: primitive 19 & 20: respect blocks and hide more Ideally, the user property should also be hidden (as leaving it in leaks information slightly), but given the schema of the note endpoint, I don't think that would be possible without introducing some kind of "ghost" user, who is attributed for posts by users who have you blocked. * fix: primitives 21, 22, and 23: reuse resolver This also increases the default `recursionLimit` for `Resolver`, as it theoretically will go higher that it previously would and could possibly fail on non-malicious collection activities. * fix: primitives 25-33: proper local instance checks * revert: fix: primitive 19 & 20 This reverts commit 465a9fe6591de90f78bd3d084e3c01e65dc3cf3c. --------- Co-authored-by: Dakkar <dakkar@thenautilus.net> Co-authored-by: Laura Hausmann <laura@hausmann.dev> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
import ms from 'ms';
|
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
import type { MiNote } from '@/models/Note.js';
|
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
|
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
|
|
import type { SchemaType } from '@/misc/json-schema.js';
|
|
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
|
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
|
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|
import { UtilityService } from '@/core/UtilityService.js';
|
|
import { bindThis } from '@/decorators.js';
|
|
import { ApiError } from '../../error.js';
|
|
|
|
export const meta = {
|
|
tags: ['federation'],
|
|
|
|
requireCredential: true,
|
|
kind: 'read:account',
|
|
|
|
limit: {
|
|
duration: ms('1hour'),
|
|
max: 30,
|
|
},
|
|
|
|
errors: {
|
|
noSuchObject: {
|
|
message: 'No such object.',
|
|
code: 'NO_SUCH_OBJECT',
|
|
id: 'dc94d745-1262-4e63-a17d-fecaa57efc82',
|
|
},
|
|
},
|
|
|
|
res: {
|
|
optional: false, nullable: false,
|
|
oneOf: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
type: {
|
|
type: 'string',
|
|
optional: false, nullable: false,
|
|
enum: ['User'],
|
|
},
|
|
object: {
|
|
type: 'object',
|
|
optional: false, nullable: false,
|
|
ref: 'UserDetailedNotMe',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
type: {
|
|
type: 'string',
|
|
optional: false, nullable: false,
|
|
enum: ['Note'],
|
|
},
|
|
object: {
|
|
type: 'object',
|
|
optional: false, nullable: false,
|
|
ref: 'Note',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
} as const;
|
|
|
|
export const paramDef = {
|
|
type: 'object',
|
|
properties: {
|
|
uri: { type: 'string' },
|
|
},
|
|
required: ['uri'],
|
|
} as const;
|
|
|
|
@Injectable()
|
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
|
constructor(
|
|
private utilityService: UtilityService,
|
|
private userEntityService: UserEntityService,
|
|
private noteEntityService: NoteEntityService,
|
|
private apResolverService: ApResolverService,
|
|
private apDbResolverService: ApDbResolverService,
|
|
private apPersonService: ApPersonService,
|
|
private apNoteService: ApNoteService,
|
|
) {
|
|
super(meta, paramDef, async (ps, me) => {
|
|
const object = await this.fetchAny(ps.uri, me);
|
|
if (object) {
|
|
return object;
|
|
} else {
|
|
throw new ApiError(meta.errors.noSuchObject);
|
|
}
|
|
});
|
|
}
|
|
|
|
/***
|
|
* URIからUserかNoteを解決する
|
|
*/
|
|
@bindThis
|
|
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
|
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
|
|
|
|
let local = await this.mergePack(me, ...await Promise.all([
|
|
this.apDbResolverService.getUserFromApId(uri),
|
|
this.apDbResolverService.getNoteFromApId(uri),
|
|
]));
|
|
if (local != null) return local;
|
|
|
|
const host = this.utilityService.extractDbHost(uri);
|
|
|
|
// local object, not found in db? fail
|
|
if (this.utilityService.isSelfHost(host)) return null;
|
|
|
|
// リモートから一旦オブジェクトフェッチ
|
|
const resolver = this.apResolverService.createResolver();
|
|
const object = await resolver.resolve(uri) as any;
|
|
|
|
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
|
// これはDBに存在する可能性があるため再度DB検索
|
|
if (uri !== object.id) {
|
|
local = await this.mergePack(me, ...await Promise.all([
|
|
this.apDbResolverService.getUserFromApId(object.id),
|
|
this.apDbResolverService.getNoteFromApId(object.id),
|
|
]));
|
|
if (local != null) return local;
|
|
}
|
|
|
|
return await this.mergePack(
|
|
me,
|
|
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
|
|
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
|
|
);
|
|
}
|
|
|
|
@bindThis
|
|
private async mergePack(me: MiLocalUser | null | undefined, user: MiUser | null | undefined, note: MiNote | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
|
|
if (user != null) {
|
|
return {
|
|
type: 'User',
|
|
object: await this.userEntityService.pack(user, me, { schema: 'UserDetailedNotMe' }),
|
|
};
|
|
} else if (note != null) {
|
|
try {
|
|
const object = await this.noteEntityService.pack(note, me, { detail: true });
|
|
|
|
return {
|
|
type: 'Note',
|
|
object,
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|