fix: primitive 13: check attribution against actor in notes

This commit is contained in:
Julia Johannesen 2024-11-14 21:17:30 -05:00
parent b9080da75d
commit c04f344049
No known key found for this signature in database
GPG key ID: 4A1377AF3E7FBC46
3 changed files with 48 additions and 29 deletions

View file

@ -436,7 +436,7 @@ export class ApInboxService {
const exist = await this.apNoteService.fetchNote(note); const exist = await this.apNoteService.fetchNote(note);
if (exist) return 'skip: note exists'; if (exist) return 'skip: note exists';
await this.apNoteService.createNote(note, resolver, silent); await this.apNoteService.createNote(note, actor, resolver, silent);
return 'ok'; return 'ok';
} catch (err) { } catch (err) {
if (err instanceof StatusError && !err.isRetryable) { if (err instanceof StatusError && !err.isRetryable) {
@ -791,7 +791,7 @@ export class ApInboxService {
await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err));
return 'ok: Question updated'; return 'ok: Question updated';
} else if (getApType(object) === 'Note') { } else if (getApType(object) === 'Note') {
await this.apNoteService.updateNote(object, resolver).catch(err => console.error(err)); await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err));
return 'ok: Note updated'; return 'ok: Note updated';
} else { } else {
return `skip: Unknown type: ${getApType(object)}`; return `skip: Unknown type: ${getApType(object)}`;

View file

@ -6,7 +6,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
@ -49,6 +49,9 @@ export class ApNoteService {
@Inject(DI.meta) @Inject(DI.meta)
private meta: MiMeta, private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.pollsRepository) @Inject(DI.pollsRepository)
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
@ -82,7 +85,13 @@ export class ApNoteService {
} }
@bindThis @bindThis
public validateNote(object: IObject, uri: string): Error | null { public validateNote(
object: IObject,
uri: string,
actor?: MiRemoteUser,
user?: MiRemoteUser,
note?: MiNote,
): Error | null {
const expectHost = this.utilityService.extractDbHost(uri); const expectHost = this.utilityService.extractDbHost(uri);
const apType = getApType(object); const apType = getApType(object);
@ -99,10 +108,27 @@ export class ApNoteService {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
} }
if (actor) {
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
if (attribution !== actor.uri) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`);
}
if (user && attribution !== user.uri) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`);
}
}
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
} }
if (note) {
const url = (object.url) ? getOneApId(object.url) : note.url;
if (url && url !== note.url) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated url does not match original url. updated url: ${url}, original url: ${note.url}`);
}
}
return null; return null;
} }
@ -120,14 +146,14 @@ export class ApNoteService {
* Noteを作成します * Noteを作成します
*/ */
@bindThis @bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> { public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value); const object = await resolver.resolve(value);
const entryUri = getApId(value); const entryUri = getApId(value);
const err = this.validateNote(object, entryUri); const err = this.validateNote(object, entryUri, actor);
if (err) { if (err) {
this.logger.error(err.message, { this.logger.error(err.message, {
resolver: { history: resolver.getHistory() }, resolver: { history: resolver.getHistory() },
@ -171,8 +197,9 @@ export class ApNoteService {
const uri = getOneApId(note.attributedTo); const uri = getOneApId(note.attributedTo);
// ローカルで投稿者を検索し、もし凍結されていたらスキップ // ローカルで投稿者を検索し、もし凍結されていたらスキップ
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; // eslint-disable-next-line no-param-reassign
if (cachedActor && cachedActor.isSuspended) { actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
if (actor && actor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
} }
@ -204,7 +231,8 @@ export class ApNoteService {
} }
//#endregion //#endregion
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; // eslint-disable-next-line no-param-reassign
actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
// 解決した投稿者が凍結されていたらスキップ // 解決した投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
@ -345,7 +373,7 @@ export class ApNoteService {
* Noteを作成します * Noteを作成します
*/ */
@bindThis @bindThis
public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> { public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
const noteUri = typeof value === 'string' ? value : value.id; const noteUri = typeof value === 'string' ? value : value.id;
if (noteUri == null) throw new Error('uri is null'); if (noteUri == null) throw new Error('uri is null');
@ -356,6 +384,9 @@ export class ApNoteService {
const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
if (UpdatedNote == null) throw new Error('Note is not registered'); if (UpdatedNote == null) throw new Error('Note is not registered');
const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null;
if (user == null) throw new Error('Note is not registered');
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
@ -372,6 +403,10 @@ export class ApNoteService {
throw err; throw err;
} }
// `validateNote` checks that the actor and user are one and the same
// eslint-disable-next-line no-param-reassign
actor ??= user;
const note = object as IPost; const note = object as IPost;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
@ -402,16 +437,7 @@ export class ApNoteService {
this.logger.info(`Creating the Note: ${note.id}`); this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ if (actor.isSuspended) {
if (note.attributedTo == null) {
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const uri = getOneApId(note.attributedTo);
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
if (cachedActor && cachedActor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
} }
@ -443,13 +469,6 @@ export class ApNoteService {
} }
//#endregion //#endregion
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
}
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
let visibility = noteAudience.visibility; let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers; const visibleUsers = noteAudience.visibleUsers;
@ -610,7 +629,7 @@ export class ApNoteService {
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
return await this.createNote(createFrom, options.resolver, true); return await this.createNote(createFrom, undefined, options.resolver, true);
} finally { } finally {
unlock(); unlock();
} }

View file

@ -140,7 +140,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.mergePack( return await this.mergePack(
me, me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null, isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null,
); );
} }