]> Untitled Git - lemmy.git/commitdiff
Various adjustments after review
authorFelix Ableitner <me@nutomic.com>
Thu, 6 Aug 2020 19:44:47 +0000 (21:44 +0200)
committerFelix Ableitner <me@nutomic.com>
Thu, 6 Aug 2020 19:44:47 +0000 (21:44 +0200)
docs/src/contributing_federation_development.md
server/src/api/mod.rs
server/src/apub/comment.rs
server/src/apub/inbox/activities/create.rs
server/src/apub/inbox/activities/remove.rs
server/src/apub/inbox/activities/undo.rs
server/src/apub/inbox/activities/update.rs
server/src/apub/inbox/community_inbox.rs
server/src/apub/inbox/shared_inbox.rs
server/src/apub/inbox/user_inbox.rs
server/src/apub/post.rs

index 143ae9f8bd1e0532bcab644fd8d09c08ca4c7e21..8af38a077d11cfe6a6ba4ae7409f20897e8b05e9 100644 (file)
@@ -68,3 +68,16 @@ cd /lemmy/
 sudo docker-compose pull
 sudo docker-compose up -d
 ```
+
+## Security Model
+
+- HTTP signature verify: This ensures that activity really comes from the activity that it claims
+- check_is_apub_valid : Makes sure its in our allowed instances list
+- Lower level checks: To make sure that the user that creates/updates/removes a post is actually on the same instance as that post
+
+For the last point, note that we are *not* checking whether the actor that sends the create activity for a post is
+actually identical to the post's creator, or that the user that removes a post is a mod/admin. These things are checked
+by the API code, and its the responsibility of each instance to check user permissions. This does not leave any attack
+vector, as a normal instance user cant do actions that violate the API rules. The only one who could do that is the
+admin (and the software deployed by the admin). But the admin can do anything on the instance, including send activities
+from other user accounts. So we wouldnt actually gain any security by checking mod permissions or similar.
\ No newline at end of file
index 7c5eeb2faf4431fa1224325cd6adaeb12a06ecc1..8124cd4a1a0f74f9d44b4f1749e67612684d5d64 100644 (file)
@@ -55,7 +55,7 @@ pub trait Perform {
   ) -> Result<Self::Response, LemmyError>;
 }
 
-pub async fn is_mod_or_admin(
+pub(in crate::api) async fn is_mod_or_admin(
   pool: &DbPool,
   user_id: i32,
   community_id: i32,
@@ -65,8 +65,7 @@ pub async fn is_mod_or_admin(
   })
   .await?;
   if !is_mod_or_admin {
-    // TODO: more accurately, not_a_mod_or_admin?
-    return Err(APIError::err("not_an_admin").into());
+    return Err(APIError::err("not_a_mod_or_admin").into());
   }
   Ok(())
 }
index 1aa3790ccf7343e8bce489256abcb2fed0fd1fde..fbec59051733e8610439e6d5d2c529795b88d2ef 100644 (file)
@@ -1,5 +1,4 @@
 use crate::{
-  api::check_slurs,
   apub::{
     activities::{generate_activity_id, send_activity_to_community},
     check_actor_domain,
@@ -50,7 +49,7 @@ use lemmy_db::{
   user::User_,
   Crud,
 };
-use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
+use lemmy_utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData};
 use log::debug;
 use serde::Deserialize;
 use serde_json::Error;
@@ -174,13 +173,13 @@ impl FromApub for CommentForm {
       .as_single_xsd_string()
       .unwrap()
       .to_string();
-    check_slurs(&content)?;
+    let content_slurs_removed = remove_slurs(&content);
 
     Ok(CommentForm {
       creator_id: creator.id,
       post_id: post.id,
       parent_id,
-      content,
+      content: content_slurs_removed,
       removed: None,
       read: None,
       published: note.published().map(|u| u.to_owned().naive_local()),
index 6e201ff3792804c320cad188b79323736a5b664a..b45f489a44ec4db9774aa6e05a5aeaa5ecfb42d3 100644 (file)
@@ -24,7 +24,6 @@ use crate::{
 };
 use activitystreams::{activity::Create, base::AnyBase, object::Note, prelude::*};
 use actix_web::{client::Client, HttpResponse};
-use anyhow::anyhow;
 use lemmy_db::{
   comment::{Comment, CommentForm},
   comment_view::CommentView,
@@ -63,10 +62,6 @@ async fn receive_create_post(
   let page = PageExt::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
 
   let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
-  // TODO: not sure if it makes sense to check for the exact user, seeing as we already check the domain
-  if post.creator_id != user.id {
-    return Err(anyhow!("Actor for create activity and post creator need to be identical").into());
-  }
 
   let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
 
@@ -99,11 +94,6 @@ async fn receive_create_comment(
   let note = Note::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
 
   let comment = CommentForm::from_apub(&note, client, pool, Some(user.actor_id()?)).await?;
-  if comment.creator_id != user.id {
-    return Err(
-      anyhow!("Actor for create activity and comment creator need to be identical").into(),
-    );
-  }
 
   let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
 
index 3db019fe2918645c3b945b0a16a28335e61535b9..b81927af199d2c9c37cf8739198fabad447dd089 100644 (file)
@@ -1,10 +1,5 @@
 use crate::{
-  api::{
-    comment::CommentResponse,
-    community::CommunityResponse,
-    is_mod_or_admin,
-    post::PostResponse,
-  },
+  api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
   apub::{
     fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
     inbox::shared_inbox::{
@@ -29,6 +24,7 @@ use crate::{
 };
 use activitystreams::{activity::Remove, base::AnyBase, object::Note, prelude::*};
 use actix_web::{client::Client, HttpResponse};
+use anyhow::anyhow;
 use lemmy_db::{
   comment::{Comment, CommentForm},
   comment_view::CommentView,
@@ -49,8 +45,9 @@ pub async fn receive_remove(
   let remove = Remove::from_any_base(activity)?.unwrap();
   let actor = get_user_from_activity(&remove, client, pool).await?;
   let community = get_community_from_activity(&remove, client, pool).await?;
-  // TODO: we dont federate remote admins at all, and remote mods arent federated properly
-  is_mod_or_admin(pool, actor.id, community.id).await?;
+  if actor.actor_id()?.domain() != community.actor_id()?.domain() {
+    return Err(anyhow!("Remove activities are only allowed on local objects").into());
+  }
 
   match remove.object().as_single_kind_str() {
     Some("Page") => receive_remove_post(remove, client, pool, chat_server).await,
index 238b1bb0f3bbf120a8328ff17c28d893ee53f04f..49e70830ae9d3714eddd31b6888302764e718381 100644 (file)
@@ -67,8 +67,8 @@ where
   let inner_actor = inner_activity.actor()?;
   let inner_actor_uri = inner_actor.as_single_xsd_any_uri().unwrap();
 
-  if outer_actor_uri != inner_actor_uri {
-    Err(anyhow!("An actor can only undo its own activities").into())
+  if outer_actor_uri.domain() != inner_actor_uri.domain() {
+    Err(anyhow!("Cant undo activities from a different instance").into())
   } else {
     Ok(())
   }
index d669e5becfeedf3834a5987f076e9c1a64b0fe84..6f2a6d56062a365007ecacbac51f159f2cb95af9 100644 (file)
@@ -25,7 +25,6 @@ use crate::{
 };
 use activitystreams::{activity::Update, base::AnyBase, object::Note, prelude::*};
 use actix_web::{client::Client, HttpResponse};
-use anyhow::anyhow;
 use lemmy_db::{
   comment::{Comment, CommentForm},
   comment_view::CommentView,
@@ -64,16 +63,11 @@ async fn receive_update_post(
   let page = PageExt::from_any_base(update.object().to_owned().one().unwrap())?.unwrap();
 
   let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
-  if post.creator_id != user.id {
-    return Err(anyhow!("Actor for update activity and post creator need to be identical").into());
-  }
 
-  let original_post = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool).await?;
-  if post.ap_id != original_post.ap_id {
-    return Err(anyhow!("Updated post ID needs to be identical to the original ID").into());
-  }
+  let original_post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
+    .await?
+    .id;
 
-  let original_post_id = original_post.id;
   blocking(pool, move |conn| {
     Post::update(conn, original_post_id, &post)
   })
@@ -107,17 +101,11 @@ async fn receive_update_comment(
   let user = get_user_from_activity(&update, client, pool).await?;
 
   let comment = CommentForm::from_apub(&note, client, pool, Some(user.actor_id()?)).await?;
-  if comment.creator_id != user.id {
-    return Err(anyhow!("Actor for update activity and post creator need to be identical").into());
-  }
 
-  let original_comment =
-    get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool).await?;
-  if comment.ap_id != original_comment.ap_id {
-    return Err(anyhow!("Updated post ID needs to be identical to the original ID").into());
-  }
+  let original_comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
+    .await?
+    .id;
 
-  let original_comment_id = original_comment.id;
   let updated_comment = blocking(pool, move |conn| {
     Comment::update(conn, original_comment_id, &comment)
   })
index 37e7c2895b8efc1970aac4db94bf643981eb6387..8b257d479bb740ebe62dbabc0f1fa9686f22fe3d 100644 (file)
@@ -69,14 +69,16 @@ pub async fn community_inbox(
 
   verify(&request, &user)?;
 
-  insert_activity(user.id, activity.clone(), false, &db).await?;
-
   let any_base = activity.clone().into_any_base()?;
   let kind = activity.kind().unwrap();
-  match kind {
-    ValidTypes::Follow => handle_follow(any_base, user, community, &client, db).await,
-    ValidTypes::Undo => handle_undo_follow(any_base, user, community, db).await,
-  }
+  let user_id = user.id;
+  let res = match kind {
+    ValidTypes::Follow => handle_follow(any_base, user, community, &client, &db).await,
+    ValidTypes::Undo => handle_undo_follow(any_base, user, community, &db).await,
+  };
+
+  insert_activity(user_id, activity.clone(), false, &db).await?;
+  res
 }
 
 /// Handle a follow request from a remote user, adding it to the local database and returning an
@@ -86,7 +88,7 @@ async fn handle_follow(
   user: User_,
   community: Community,
   client: &Client,
-  db: DbPoolParam,
+  db: &DbPoolParam,
 ) -> Result<HttpResponse, LemmyError> {
   let follow = Follow::from_any_base(activity)?.unwrap();
   let community_follower_form = CommunityFollowerForm {
@@ -95,12 +97,12 @@ async fn handle_follow(
   };
 
   // This will fail if they're already a follower, but ignore the error.
-  blocking(&db, move |conn| {
+  blocking(db, move |conn| {
     CommunityFollower::follow(&conn, &community_follower_form).ok()
   })
   .await?;
 
-  community.send_accept_follow(follow, &client, &db).await?;
+  community.send_accept_follow(follow, &client, db).await?;
 
   Ok(HttpResponse::Ok().finish())
 }
@@ -109,7 +111,7 @@ async fn handle_undo_follow(
   activity: AnyBase,
   user: User_,
   community: Community,
-  db: DbPoolParam,
+  db: &DbPoolParam,
 ) -> Result<HttpResponse, LemmyError> {
   let _undo = Undo::from_any_base(activity)?.unwrap();
 
@@ -119,7 +121,7 @@ async fn handle_undo_follow(
   };
 
   // This will fail if they aren't a follower, but ignore the error.
-  blocking(&db, move |conn| {
+  blocking(db, move |conn| {
     CommunityFollower::unfollow(&conn, &community_follower_form).ok()
   })
   .await?;
index 5406d11195492915b0b1120e54bdecc47a6de680..d5ed9312d862d3fecfdce48a7bd581d91a72ebff 100644 (file)
@@ -77,12 +77,9 @@ pub async fn shared_inbox(
   check_is_apub_id_valid(sender)?;
   verify(&request, actor.as_ref())?;
 
-  // TODO: probably better to do this after, so we dont store activities that fail a check somewhere
-  insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
-
   let any_base = activity.clone().into_any_base()?;
   let kind = activity.kind().unwrap();
-  match kind {
+  let res = match kind {
     ValidTypes::Announce => receive_announce(any_base, &client, &pool, chat_server).await,
     ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
     ValidTypes::Update => receive_update(any_base, &client, &pool, chat_server).await,
@@ -91,7 +88,10 @@ pub async fn shared_inbox(
     ValidTypes::Remove => receive_remove(any_base, &client, &pool, chat_server).await,
     ValidTypes::Delete => receive_delete(any_base, &client, &pool, chat_server).await,
     ValidTypes::Undo => receive_undo(any_base, &client, &pool, chat_server).await,
-  }
+  };
+
+  insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
+  res
 }
 
 pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(
index 53f1fad2c79ce51079fedc7c308a55e74ec1f442..b443b51c840fa870c2404847b42082fed9158683 100644 (file)
@@ -65,11 +65,9 @@ pub async fn user_inbox(
   let actor = get_or_fetch_and_upsert_actor(actor_uri, &client, &pool).await?;
   verify(&request, actor.as_ref())?;
 
-  insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
-
   let any_base = activity.clone().into_any_base()?;
   let kind = activity.kind().unwrap();
-  match kind {
+  let res = match kind {
     ValidTypes::Accept => receive_accept(any_base, username, &client, &pool).await,
     ValidTypes::Create => {
       receive_create_private_message(any_base, &client, &pool, chat_server).await
@@ -83,7 +81,10 @@ pub async fn user_inbox(
     ValidTypes::Undo => {
       receive_undo_delete_private_message(any_base, &client, &pool, chat_server).await
     }
-  }
+  };
+
+  insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
+  res
 }
 
 /// Handle accepted follows.
index a2a83f4532a9f81a36f832ae696bca11236de316..8bd8364f120a379fec9a46c9c048d7ae8bd6b43d 100644 (file)
@@ -1,5 +1,5 @@
 use crate::{
-  api::{check_slurs, check_slurs_opt},
+  api::check_slurs,
   apub::{
     activities::{generate_activity_id, send_activity_to_community},
     check_actor_domain,
@@ -44,7 +44,7 @@ use lemmy_db::{
   user::User_,
   Crud,
 };
-use lemmy_utils::convert_datetime;
+use lemmy_utils::{convert_datetime, remove_slurs};
 use serde::Deserialize;
 use url::Url;
 
@@ -225,11 +225,11 @@ impl FromApub for PostForm {
       .as_ref()
       .map(|c| c.as_single_xsd_string().unwrap().to_string());
     check_slurs(&name)?;
-    check_slurs_opt(&body)?;
+    let body_slurs_removed = body.map(|b| remove_slurs(&b));
     Ok(PostForm {
       name,
       url,
-      body,
+      body: body_slurs_removed,
       creator_id: creator.id,
       community_id: community.id,
       removed: None,