From 08fdc93d35c0a297c631534301eb691b04443bdb Mon Sep 17 00:00:00 2001
From: alemi <me@alemi.dev>
Date: Tue, 28 Jan 2025 13:55:20 +0100
Subject: [PATCH] feat: optionally fetch and verify relayed activity

relays usually Announce(Create), so the Create is not from them but the
announce is, and it gets processed properly. Lemmy does the correct
thing: it sends Announce(...activity...), so the "topmost" activity
effectively comes from the sending server and can be verified. however
aode relay sends activities as-is, without wrapping. so if we receive
activities from someone else, it won't match the http signature and
we thus can't be sure this wasn't falsified. added an option to directly
fetch such cases. it's probably not great, so defaults to OFF
---
 core/src/config.rs              |  7 +++++++
 routes/src/activitypub/inbox.rs | 10 +++++++---
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/core/src/config.rs b/core/src/config.rs
index 2db195b..be8ec27 100644
--- a/core/src/config.rs
+++ b/core/src/config.rs
@@ -139,6 +139,13 @@ pub struct CompatibilityConfig {
 	#[serde_inline_default(true)]
 	/// compatibility with lemmy: avoid showing images twice
 	pub skip_single_attachment_if_image_is_set: bool,
+
+	#[serde_inline_default(false)]
+	/// compatibility with most relays: since they send us other server's activities, we must fetch
+	/// them to verify that they aren't falsified by the relay itself. this is quite expensive, as
+	/// relays send a lot of activities and we effectively end up fetching again all these, so this
+	/// defaults to false
+	pub verify_relayed_activities_by_fetching: bool,
 }
 
 #[serde_inline_default::serde_inline_default]
diff --git a/routes/src/activitypub/inbox.rs b/routes/src/activitypub/inbox.rs
index 4253d1f..4666203 100644
--- a/routes/src/activitypub/inbox.rs
+++ b/routes/src/activitypub/inbox.rs
@@ -1,7 +1,7 @@
 use apb::{Activity, ActivityType, Base};
 use axum::{extract::{Query, State}, http::StatusCode, Json};
 use sea_orm::{sea_query::IntoCondition, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
-use upub::{model::job::JobType, selector::{RichActivity, RichFillable}, Context};
+use upub::{model::job::JobType, selector::{RichActivity, RichFillable}, traits::Fetcher, Context};
 
 use crate::{AuthIdentity, Identity, builders::JsonLD};
 
@@ -41,7 +41,7 @@ pub async fn page(
 pub async fn post(
 	State(ctx): State<Context>,
 	AuthIdentity(auth): AuthIdentity,
-	Json(activity): Json<serde_json::Value>
+	Json(mut activity): Json<serde_json::Value>
 ) -> crate::ApiResult<StatusCode> {
 	let Identity::Remote { domain, user: uid, .. } = auth else {
 		if matches!(activity.activity_type(), Ok(ActivityType::Delete)) {
@@ -72,7 +72,11 @@ pub async fn post(
 	let server = upub::Context::server(&aid);
 
 	if activity.actor().id()? != uid {
-		return Err(crate::ApiError::forbidden());
+		if ctx.cfg().compat.verify_relayed_activities_by_fetching {
+			activity = ctx.pull(&activity.id()?).await?.activity()?;
+		} else {
+			return Err(crate::ApiError::forbidden());
+		}
 	}
 
 	if let Some(_internal) = upub::model::activity::Entity::ap_to_internal(&aid, ctx.db()).await? {