From 544b5a1678c37895cd3734200f521817d0241eec Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Fri, 20 Oct 2023 20:06:25 +0200 Subject: [PATCH] [backend] Use a semaphore around populateMentions This fixes a user-generated DoS payload for giant webring-style trees of mentions in user bios that could cause backend stalls. --- .pnp.cjs | 22 +++++++++++++++++++ ...-mutex-npm-0.4.0-f5a25d4255-813a71728b.zip | 3 +++ .../tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip | 3 +++ packages/backend/package.json | 1 + .../src/models/repositories/user-profile.ts | 15 ++++++++----- yarn.lock | 17 ++++++++++++++ 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 .yarn/cache/async-mutex-npm-0.4.0-f5a25d4255-813a71728b.zip create mode 100644 .yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip diff --git a/.pnp.cjs b/.pnp.cjs index 5d1cd10e7..272945ae3 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -1722,6 +1722,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "async-lock",\ "npm:1.4.0"\ ],\ + [\ + "async-mutex",\ + "npm:0.4.0"\ + ],\ [\ "async-settle",\ "npm:1.0.0"\ @@ -13423,6 +13427,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["async-mutex", [\ + ["npm:0.4.0", {\ + "packageLocation": "./.yarn/cache/async-mutex-npm-0.4.0-f5a25d4255-813a71728b.zip/node_modules/async-mutex/",\ + "packageDependencies": [\ + ["async-mutex", "npm:0.4.0"],\ + ["tslib", "npm:2.6.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["async-settle", [\ ["npm:1.0.0", {\ "packageLocation": "./.yarn/cache/async-settle-npm-1.0.0-5d08fbf926-d2382ad4b9.zip/node_modules/async-settle/",\ @@ -13862,6 +13876,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["archiver", "npm:5.3.1"],\ ["argon2", "npm:0.30.3"],\ ["async-lock", "npm:1.4.0"],\ + ["async-mutex", "npm:0.4.0"],\ ["autolinker", "npm:4.0.0"],\ ["autwh", "npm:0.1.0"],\ ["aws-sdk", "npm:2.1413.0"],\ @@ -30575,6 +30590,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["tslib", "npm:2.6.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.6.2", {\ + "packageLocation": "./.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip/node_modules/tslib/",\ + "packageDependencies": [\ + ["tslib", "npm:2.6.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["tsscmp", [\ diff --git a/.yarn/cache/async-mutex-npm-0.4.0-f5a25d4255-813a71728b.zip b/.yarn/cache/async-mutex-npm-0.4.0-f5a25d4255-813a71728b.zip new file mode 100644 index 000000000..d93c14353 --- /dev/null +++ b/.yarn/cache/async-mutex-npm-0.4.0-f5a25d4255-813a71728b.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e2b7ebf1312c0815d655985acf66c936a1d54cf9c7791697eeb37ce8666d867 +size 20171 diff --git a/.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip b/.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip new file mode 100644 index 000000000..0a4c4da21 --- /dev/null +++ b/.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4eaf40132ddbb49047d715f9d0bdda22e1460f5a071ebe69adeecf55c6be5d8 +size 26533 diff --git a/packages/backend/package.json b/packages/backend/package.json index d47d386a3..28f5adcae 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -41,6 +41,7 @@ "archiver": "5.3.1", "argon2": "^0.30.3", "async-lock": "1.4.0", + "async-mutex": "^0.4.0", "autolinker": "4.0.0", "autwh": "0.1.0", "aws-sdk": "2.1413.0", diff --git a/packages/backend/src/models/repositories/user-profile.ts b/packages/backend/src/models/repositories/user-profile.ts index 09e28b695..9579a68a0 100644 --- a/packages/backend/src/models/repositories/user-profile.ts +++ b/packages/backend/src/models/repositories/user-profile.ts @@ -6,8 +6,12 @@ import { resolveMentionToUserAndProfile } from "@/remote/resolve-user.js"; import { IMentionedRemoteUsers } from "@/models/entities/note.js"; import { unique } from "@/prelude/array.js"; import config from "@/config/index.js"; +import { Semaphore } from "async-mutex"; + +const queue = new Semaphore(10); export const UserProfileRepository = db.getRepository(UserProfile).extend({ + // We must never await this without promiseEarlyReturn, otherwise giant webring-style profile mention trees will cause the queue to stop working async updateMentions(id: UserProfile["userId"]){ const profile = await this.findOneBy({ userId: id }); if (!profile) return; @@ -18,11 +22,12 @@ export const UserProfileRepository = db.getRepository(UserProfile).extend({ if (profile.fields.length > 0) tokens.push(...profile.fields.map(p => mfm.parse(p.value).concat(mfm.parse(p.name))).flat()); - const partial = { - mentions: await populateMentions(tokens, profile.userHost) - }; - - return UserProfileRepository.update(profile.userId, partial) + return queue.runExclusive(async () => { + const partial = { + mentions: await populateMentions(tokens, profile.userHost) + }; + return UserProfileRepository.update(profile.userId, partial); + }); }, }); diff --git a/yarn.lock b/yarn.lock index 15f3ad67b..9d8dd6e8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4779,6 +4779,15 @@ __metadata: languageName: node linkType: hard +"async-mutex@npm:^0.4.0": + version: 0.4.0 + resolution: "async-mutex@npm:0.4.0" + dependencies: + tslib: ^2.4.0 + checksum: 813a71728b35a4fbfd64dba719f04726d9133c67b577fcd951b7028c4a675a13ee34e69beb82d621f87bf81f5d4f135c4c44be0448550c7db728547244ef71fc + languageName: node + linkType: hard + "async-settle@npm:^1.0.0": version: 1.0.0 resolution: "async-settle@npm:1.0.0" @@ -5129,6 +5138,7 @@ __metadata: archiver: 5.3.1 argon2: ^0.30.3 async-lock: 1.4.0 + async-mutex: ^0.4.0 autolinker: 4.0.0 autwh: 0.1.0 aws-sdk: 2.1413.0 @@ -19712,6 +19722,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.4.0": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad + languageName: node + linkType: hard + "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6"