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? {