Ignore old federated post edits (ref #4529)

pull/4586/head
Felix Ableitner 2 months ago
parent 60f9a97dfa
commit 240294f98f

@ -2,7 +2,7 @@ use crate::{
activities::{verify_is_public, verify_person_in_community},
check_apub_id_valid_with_strictness,
mentions::collect_non_local_mentions,
objects::{read_from_string_or_source, verify_is_remote_object},
objects::{read_from_string_or_source, verify_is_remote_object, verify_object_timestamp},
protocol::{
objects::{note::Note, LanguageTag},
InCommunity,
@ -141,6 +141,15 @@ impl Object for ApubComment {
check_apub_id_valid_with_strictness(note.id.inner(), community.local, context).await?;
verify_is_remote_object(note.id.inner(), context.settings())?;
verify_person_in_community(&note.attributed_to, &community, context).await?;
let old_comment = note.id.dereference_local(context).await;
let old_timestamp = old_comment
.as_ref()
.map(|c| c.updated.unwrap_or(c.published))
.clone()
.ok();
verify_object_timestamp(old_timestamp, note.updated.or(note.published))?;
let (post, _) = note.get_parents(context).await?;
let creator = note.attributed_to.dereference(context).await?;
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &creator, community.id)

@ -1,6 +1,7 @@
use crate::protocol::Source;
use activitypub_federation::protocol::values::MediaTypeMarkdownOrHtml;
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use html2md::parse_html;
use lemmy_utils::{error::LemmyError, settings::structs::Settings};
use url::Url;
@ -51,3 +52,34 @@ pub(crate) fn verify_is_remote_object(id: &Url, settings: &Settings) -> Result<(
Ok(())
}
}
/// When receiving a federated object, check that the timestamp is newer than the latest version stored
/// locally. Necessary to reject edits which are received in wrong order.
pub(crate) fn verify_object_timestamp(
old_timestamp: Option<DateTime<Utc>>,
new_timestamp: Option<DateTime<Utc>>,
) -> Result<(), LemmyError> {
if let (Some(old_timestamp), Some(new_timestamp)) = (old_timestamp, new_timestamp) {
if new_timestamp < old_timestamp {
return Err(anyhow!("Ignoring old object edit").into());
}
}
Ok(())
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_verify_object_timestamp() {
let old = Some(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
let new = Some(Utc.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap());
assert!(verify_object_timestamp(old, new).is_ok());
assert!(verify_object_timestamp(None, new).is_ok());
assert!(verify_object_timestamp(old, None).is_ok());
assert!(verify_object_timestamp(new, old).is_err());
}
}

@ -2,7 +2,7 @@ use crate::{
activities::{verify_is_public, verify_person_in_community},
check_apub_id_valid_with_strictness,
local_site_data_cached,
objects::{read_from_string_or_source_opt, verify_is_remote_object},
objects::{read_from_string_or_source_opt, verify_is_remote_object, verify_object_timestamp},
protocol::{
objects::{
page::{Attachment, AttributedTo, Hashtag, HashtagType, Page, PageType},
@ -216,6 +216,13 @@ impl Object for ApubPost {
// read existing, local post if any (for generating mod log)
let old_post = page.id.dereference_local(context).await;
let old_timestamp = old_post
.as_ref()
.map(|p| p.updated.unwrap_or(p.published))
.clone()
.ok();
verify_object_timestamp(old_timestamp, page.updated.or(page.published))?;
let first_attachment = page.attachment.first();
let local_site = LocalSite::read(&mut context.pool()).await.ok();

@ -1,6 +1,6 @@
use crate::{
check_apub_id_valid_with_strictness,
objects::read_from_string_or_source,
objects::{read_from_string_or_source, verify_object_timestamp},
protocol::{
objects::chat_message::{ChatMessage, ChatMessageType},
Source,
@ -105,6 +105,14 @@ impl Object for ApubPrivateMessage {
verify_domains_match(note.id.inner(), expected_domain)?;
verify_domains_match(note.attributed_to.inner(), note.id.inner())?;
let old_pm = note.id.dereference_local(context).await;
let old_timestamp = old_pm
.as_ref()
.map(|c| c.updated.unwrap_or(c.published))
.clone()
.ok();
verify_object_timestamp(old_timestamp, note.updated.or(note.published))?;
check_apub_id_valid_with_strictness(note.id.inner(), false, context).await?;
let person = note.attributed_to.dereference(context).await?;
if person.banned {

@ -7,7 +7,7 @@ use crate::{
community_outbox::ApubCommunityOutbox,
},
local_site_data_cached,
objects::{community::ApubCommunity, read_from_string_or_source_opt},
objects::{community::ApubCommunity, read_from_string_or_source_opt, verify_object_timestamp},
protocol::{
objects::{Endpoints, LanguageTag},
ImageObject,
@ -15,6 +15,7 @@ use crate::{
},
};
use activitypub_federation::{
config::Data,
fetch::{collection_id::CollectionId, object_id::ObjectId},
kinds::actor::GroupType,
protocol::{
@ -75,7 +76,7 @@ impl Group {
pub(crate) async fn verify(
&self,
expected_domain: &Url,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
check_apub_id_valid_with_strictness(self.id.inner(), true, context).await?;
verify_domains_match(expected_domain, self.id.inner())?;
@ -87,6 +88,15 @@ impl Group {
check_slurs_opt(&self.name, slur_regex)?;
let description = read_from_string_or_source_opt(&self.summary, &None, &self.source);
check_slurs_opt(&description, slur_regex)?;
let old_communmity = self.id.dereference_local(context).await;
let old_timestamp = old_communmity
.as_ref()
.map(|c| c.updated.unwrap_or(c.published))
.clone()
.ok();
verify_object_timestamp(old_timestamp, self.updated.or(self.published))?;
Ok(())
}
}

Loading…
Cancel
Save