CVE-2024-23649: Insufficient Information (NVD-CWE-noinfo)
value: 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.

Creating 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:

Any 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.

Version 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 6.5
Published: 2024-01-24T18:15Z
Repository: https://github.com/LemmyNet/lemmy
Language: TypeScript
Files changed: api_tests/prepare-drone-federation-test.sh, api_tests/src/private_message.spec.ts, api_tests/src/shared.ts, crates/api/src/private_message_report/create.rs
Commit date: 2023-12-18 10:25:05 +0100
Commit message: Fixing private message reports. (#4279)


Diff: diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh
index 0f7591b3..4710168d 100755
--- a/api_tests/prepare-drone-federation-test.sh
+++ b/api_tests/prepare-drone-federation-test.sh
@@ -7,11 +7,11 @@ export RUST_BACKTRACE=1
 export RUST_LOG="warn,lemmy_server=debug,lemmy_federate=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
 
 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
 ./api_tests/pict-rs \
   run -a 0.0.0.0:8080 \
diff --git a/api_tests/src/private_message.spec.ts b/api_tests/src/private_message.spec.ts
index 08c519df..75dcaee3 100644
--- a/api_tests/src/private_message.spec.ts
+++ b/api_tests/src/private_message.spec.ts
@@ -8,10 +8,11 @@ import {
   editPrivateMessage,
   listPrivateMessages,
   deletePrivateMessage,
   unfollowRemotes,
   waitUntil,
+  reportPrivateMessage,
 } from "./shared";
 
 let recipient_id: number;
 
 beforeAll(async () => {
@@ -107,5 +108,44 @@ test("Delete a private message", async () => {
   );
   expect(betaPms3.private_messages.length).toBe(
     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,
+  );
+});
diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts
index 0545eee5..fe51fb04 100644
--- a/api_tests/src/shared.ts
+++ b/api_tests/src/shared.ts
@@ -2,16 +2,18 @@ import {
   BlockCommunity,
   BlockCommunityResponse,
   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";
 import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
 import { EditPost } from "lemmy-js-client/dist/types/EditPost";
@@ -779,10 +781,22 @@ export async function reportComment(
     reason,
   };
   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> {
   let form: ListCommentReports = {};
   return api.listCommentReports(form);
diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/private_message_report/create.rs
index 75620bf8..7aca9661 100644
--- a/crates/api/src/private_message_report/create.rs
+++ b/crates/api/src/private_message_report/create.rs
@@ -29,10 +29,15 @@ pub async fn create_pm_report(
 
   let person_id = local_user_view.person.id;
   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,
     original_pm_text: private_message.content,
     reason,

Vulnerable code: if ! [ -f "pict-rs" ]; then
Fixed code: if [ ! -f "pict-rs" ]; then
  reportPrivateMessage,

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,
  );
});
  CreatePrivateMessageReport,
  PrivateMessageReportResponse,
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);
}

  // Make sure that only the recipient of the private message can create a report
  if person_id != private_message.recipient_id {
    Err(LemmyErrorType::CouldntCreateReport)?
  }