fix(server): "forkbomb" DOS mitigation (#9247)

* Add recursion limit to resolver

* Use shared resolver in featured and question

* Changelog

* Changelog fix

* Update CHANGELOG.md

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Add host to recursion limit error message

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
Derek 2022-12-02 16:13:36 -05:00 committed by GitHub
parent 5decad9cf1
commit 66513b9893
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 14 additions and 8 deletions

View file

@ -4,7 +4,7 @@
### Improvements ### Improvements
### Bugfixes ### Bugfixes
- -
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
@ -25,6 +25,7 @@ You should also include the user name that made the change.
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
- Server: Bug fix for Pinned Users lookup on instance @squidicuzz - Server: Bug fix for Pinned Users lookup on instance @squidicuzz
- Client: インスタンスティッカーのfaviconを読み込む際に偽サイト警告が出ることがあるのを修正 @syuilo - Client: インスタンスティッカーのfaviconを読み込む際に偽サイト警告が出ることがあるのを修正 @syuilo
- Server: Mitigate AP reference chain DoS vector @skehmatics
## 12.119.0 (2022/09/10) ## 12.119.0 (2022/09/10)

View file

@ -731,7 +731,7 @@ export class ApInboxService {
await this.apPersonService.updatePerson(actor.uri!, resolver, object); await this.apPersonService.updatePerson(actor.uri!, resolver, object);
return 'ok: Person updated'; return 'ok: Person updated';
} else if (getApType(object) === 'Question') { } else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object).catch(err => console.error(err)); await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
return 'ok: Question updated'; return 'ok: Question updated';
} else { } else {
return `skip: Unknown type: ${getApType(object)}`; return `skip: Unknown type: ${getApType(object)}`;

View file

@ -76,6 +76,7 @@ export class Resolver {
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService, private apDbResolverService: ApDbResolverService,
private recursionLimit = 100
) { ) {
this.history = new Set(); this.history = new Set();
} }
@ -116,6 +117,10 @@ export class Resolver {
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
if (this.history.size > this.recursionLimit) {
throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
}
this.history.add(value); this.history.add(value);
const host = this.utilityService.extractDbHost(value); const host = this.utilityService.extractDbHost(value);

View file

@ -390,7 +390,7 @@ export class ApPersonService implements OnModuleInit {
}); });
//#endregion //#endregion
await this.updateFeatured(user!.id).catch(err => this.logger.error(err)); await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user!; return user!;
} }
@ -503,7 +503,7 @@ export class ApPersonService implements OnModuleInit {
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
}); });
await this.updateFeatured(exist.id).catch(err => this.logger.error(err)); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
} }
/** /**
@ -551,14 +551,14 @@ export class ApPersonService implements OnModuleInit {
return { fields, services }; return { fields, services };
} }
public async updateFeatured(userId: User['id']) { public async updateFeatured(userId: User['id'], resolver?: Resolver) {
const user = await this.usersRepository.findOneByOrFail({ id: userId }); const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (!this.userEntityService.isRemoteUser(user)) return; if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
this.logger.info(`Updating the featured: ${user.uri}`); this.logger.info(`Updating the featured: ${user.uri}`);
const resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
// Resolve to (Ordered)Collection Object // Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured); const collection = await resolver.resolveCollection(user.featured);

View file

@ -65,7 +65,7 @@ export class ApQuestionService {
* @param uri URI of AP Question object * @param uri URI of AP Question object
* @returns true if updated * @returns true if updated
*/ */
public async updateQuestion(value: any) { public async updateQuestion(value: any, resolver?: Resolver) {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -80,7 +80,7 @@ export class ApQuestionService {
//#endregion //#endregion
// resolve new Question object // resolve new Question object
const resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(value) as IQuestion; const question = await resolver.resolve(value) as IQuestion;
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);