CVE-2024-23649 (published 2024-01-24T18:15Z)

Lemmy is a link aggregator and forum for the fediverse. Starting in version 0.17.0 and prior to version 0.19.1, users can report private messages, even when they're neither sender nor recipient of the message. The API response to creating a private message report contains the private message itself, which means any user can just iterate over message ids to (loudly) obtain all private messages of an instance. A user with instance admin privileges can also abuse this if the private message is removed from the response, as they're able to see the resulting reports.\n\nCreating a private message report by POSTing to `/api/v3/private_message/report` does not validate whether the reporter is the recipient of the message. lemmy-ui does not allow the sender to report the message; the API method should likely be restricted to accessible to recipients only. The API response when creating a report contains the `private_message_report_view` with all the details of the report, including the private message that has been reported:\n\nAny authenticated user can obtain arbitrary (untargeted) private message contents. Privileges required depend on the instance configuration; when registrations are enabled without application system, the privileges required are practically none. When registration applications are required, privileges required could be considered low, but this assessment heavily varies by instance.\n\nVersion 0.19.1 contains a patch for this issue. A workaround is available. If an update to a fixed Lemmy version is not immediately possible, the API route can be blocked in the reverse proxy. This will prevent anyone from reporting private messages, but it will also prevent exploitation before the update has been applied.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/LemmyNet/lemmy
commit_details: repo_url: https://github.com/LemmyNet/lemmy
author: Dessalines
author_date: 2023-12-18 04:25:05-05:00
msg: Fixing private message reports. (#4279)
repository: repo_url: https://github.com/LemmyNet/lemmy
repo_name: LemmyNet/lemmy
description: 🐀 A link aggregator and forum for the fediverse
repo_language: Rust
owner: LemmyNet
file_changes: filename: prepare-drone-federation-test.sh
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ export RUST_LOG="warn,lemmy_server=debug,lemmy_federate=debug,lemmy_api=debug,le
 export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
 
 # pictrs setup
-if ! [ -f "pict-rs" ]; then
+if [ ! -f "pict-rs" ]; then
   curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs
   chmod +x api_tests/pict-rs
 fi

programming_language: Shell
filename: private_message.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,7 @@ import {
   deletePrivateMessage,
   unfollowRemotes,
   waitUntil,
+  reportPrivateMessage,
 } from "./shared";
 
 let recipient_id: number;
@@ -109,3 +110,42 @@ test("Delete a private message", async () => {
     betaPms1.private_messages.length,
   );
 });
+
+test("Create a private message report", async () => {
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  let betaPms1 = await waitUntil(
+    () => listPrivateMessages(beta),
+    m =>
+      !!m.private_messages.find(
+        e =>
+          e.private_message.ap_id ===
+          pmRes.private_message_view.private_message.ap_id,
+      ),
+  );
+  let betaPm = betaPms1.private_messages[0];
+  expect(betaPm).toBeDefined();
+
+  // Make sure that only the recipient can report it, so this should fail
+  await expect(
+    reportPrivateMessage(
+      alpha,
+      pmRes.private_message_view.private_message.id,
+      "a reason",
+    ),
+  ).rejects.toStrictEqual(Error("couldnt_create_report"));
+
+  // This one should pass
+  let reason = "another reason";
+  let report = await reportPrivateMessage(
+    beta,
+    betaPm.private_message.id,
+    reason,
+  );
+
+  expect(report.private_message_report_view.private_message.id).toBe(
+    betaPm.private_message.id,
+  );
+  expect(report.private_message_report_view.private_message_report.reason).toBe(
+    reason,
+  );
+});

programming_language: TypeScript
filename: shared.ts
change_type: ModificationType.MODIFY
diff: @@ -4,12 +4,14 @@ import {
   BlockInstance,
   BlockInstanceResponse,
   CommunityId,
+  CreatePrivateMessageReport,
   GetReplies,
   GetRepliesResponse,
   GetUnreadCountResponse,
   InstanceId,
   LemmyHttp,
   PostView,
+  PrivateMessageReportResponse,
   SuccessResponse,
 } from "lemmy-js-client";
 import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
@@ -781,6 +783,18 @@ export async function reportComment(
   return api.createCommentReport(form);
 }
 
+export async function reportPrivateMessage(
+  api: LemmyHttp,
+  private_message_id: number,
+  reason: string,
+): Promise<PrivateMessageReportResponse> {
+  let form: CreatePrivateMessageReport = {
+    private_message_id,
+    reason,
+  };
+  return api.createPrivateMessageReport(form);
+}
+
 export async function listCommentReports(
   api: LemmyHttp,
 ): Promise<ListCommentReportsResponse> {

programming_language: TypeScript
filename: create.rs
change_type: ModificationType.MODIFY
diff: @@ -31,6 +31,11 @@ pub async fn create_pm_report(
   let private_message_id = data.private_message_id;
   let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
 
+  // Make sure that only the recipient of the private message can create a report
+  if person_id != private_message.recipient_id {
+    Err(LemmyErrorType::CouldntCreateReport)?
+  }
+
   let report_form = PrivateMessageReportForm {
     creator_id: person_id,
     private_message_id,

programming_language: Rust