diff --git a/lemmy_utils/src/utils.rs b/lemmy_utils/src/utils.rs index 87aad574a..eeaccd5b0 100644 --- a/lemmy_utils/src/utils.rs +++ b/lemmy_utils/src/utils.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, FixedOffset, Local, NaiveDateTime}; use itertools::Itertools; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use regex::{Regex, RegexBuilder}; +use url::Url; lazy_static! { static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); @@ -128,3 +129,14 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String { .unwrap_or("127.0.0.1") .to_string() } + +pub fn is_valid_url_opt(value: &Option) -> Result<(), APIError> { + if let Some(v) = value { + match Url::parse(v) { + Ok(_t) => Ok(()), + Err(_e) => Err(APIError::err("invalid_url")), + } + } else { + Ok(()) + } +} diff --git a/src/api/comment.rs b/src/api/comment.rs index 69853c3e7..650df43b3 100644 --- a/src/api/comment.rs +++ b/src/api/comment.rs @@ -11,6 +11,7 @@ use crate::{ LemmyContext, }; use actix_web::web::Data; +use ammonia::clean_text; use lemmy_db::{ comment::*, comment_view::*, @@ -54,7 +55,7 @@ impl Perform for CreateComment { let content_slurs_removed = remove_slurs(&data.content.to_owned()); let comment_form = CommentForm { - content: content_slurs_removed, + content: clean_text(&content_slurs_removed), parent_id: data.parent_id.to_owned(), post_id: data.post_id, creator_id: user.id, @@ -364,7 +365,7 @@ impl Perform for RemoveComment { mod_user_id: user.id, comment_id: data.edit_id, removed: Some(removed), - reason: data.reason.to_owned(), + reason: data.reason.as_ref().map(|r| clean_text(r)), }; blocking(context.pool(), move |conn| { ModRemoveComment::create(conn, &form) diff --git a/src/api/community.rs b/src/api/community.rs index 77a945a64..c1843ead3 100644 --- a/src/api/community.rs +++ b/src/api/community.rs @@ -4,6 +4,7 @@ use crate::{ LemmyContext, }; use actix_web::web::Data; +use ammonia::clean_text; use anyhow::Context; use lemmy_db::{ comment::Comment, @@ -35,7 +36,13 @@ use lemmy_structs::{ use lemmy_utils::{ apub::{generate_actor_keypair, make_apub_endpoint, EndpointType}, location_info, - utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix}, + utils::{ + check_slurs, + check_slurs_opt, + is_valid_community_name, + is_valid_url_opt, + naive_from_unix, + }, APIError, ConnectionId, LemmyError, @@ -139,10 +146,13 @@ impl Perform for CreateCommunity { // When you create a community, make sure the user becomes a moderator and a follower let keypair = generate_actor_keypair()?; + is_valid_url_opt(&data.icon)?; + is_valid_url_opt(&data.banner)?; + let community_form = CommunityForm { - name: data.name.to_owned(), - title: data.title.to_owned(), - description: data.description.to_owned(), + name: clean_text(&data.name), + title: clean_text(&data.title), + description: data.description.as_ref().map(|d| clean_text(d)), icon: Some(data.icon.to_owned()), banner: Some(data.banner.to_owned()), category_id: data.category_id, @@ -233,10 +243,13 @@ impl Perform for EditCommunity { let icon = diesel_option_overwrite(&data.icon); let banner = diesel_option_overwrite(&data.banner); + is_valid_url_opt(&data.icon)?; + is_valid_url_opt(&data.banner)?; + let community_form = CommunityForm { - name: read_community.name, - title: data.title.to_owned(), - description: data.description.to_owned(), + name: clean_text(&read_community.name), + title: clean_text(&data.title), + description: data.description.as_ref().map(|d| clean_text(d)), icon, banner, category_id: data.category_id.to_owned(), @@ -375,7 +388,7 @@ impl Perform for RemoveCommunity { mod_user_id: user.id, community_id: data.edit_id, removed: Some(removed), - reason: data.reason.to_owned(), + reason: data.reason.as_ref().map(|r| clean_text(r)), expires, }; blocking(context.pool(), move |conn| { @@ -611,7 +624,7 @@ impl Perform for BanFromCommunity { mod_user_id: user.id, other_user_id: data.user_id, community_id: data.community_id, - reason: data.reason.to_owned(), + reason: data.reason.as_ref().map(|r| clean_text(r)), banned: Some(data.ban), expires, }; diff --git a/src/api/post.rs b/src/api/post.rs index ef5409d1f..ed589796d 100644 --- a/src/api/post.rs +++ b/src/api/post.rs @@ -5,6 +5,7 @@ use crate::{ LemmyContext, }; use actix_web::web::Data; +use ammonia::clean_text; use lemmy_db::{ comment_view::*, community_view::*, @@ -67,9 +68,9 @@ impl Perform for CreatePost { fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await; let post_form = PostForm { - name: data.name.trim().to_owned(), + name: clean_text(&data.name.trim()), url: data.url.to_owned(), - body: data.body.to_owned(), + body: data.body.as_ref().map(|b| clean_text(&b)), community_id: data.community_id, creator_id: user.id, removed: None, @@ -379,9 +380,9 @@ impl Perform for EditPost { fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await; let post_form = PostForm { - name: data.name.trim().to_owned(), + name: clean_text(&data.name.trim()), url: data.url.to_owned(), - body: data.body.to_owned(), + body: data.body.as_ref().map(|b| clean_text(b)), nsfw: data.nsfw, creator_id: orig_post.creator_id.to_owned(), community_id: orig_post.community_id, @@ -527,7 +528,7 @@ impl Perform for RemovePost { mod_user_id: user.id, post_id: data.edit_id, removed: Some(removed), - reason: data.reason.to_owned(), + reason: data.reason.as_ref().map(|r| clean_text(r)), }; blocking(context.pool(), move |conn| { ModRemovePost::create(conn, &form) diff --git a/src/api/site.rs b/src/api/site.rs index 3ff1e49d0..ac5561a04 100644 --- a/src/api/site.rs +++ b/src/api/site.rs @@ -5,6 +5,7 @@ use crate::{ LemmyContext, }; use actix_web::web::Data; +use ammonia::clean_text; use anyhow::Context; use lemmy_db::{ category::*, @@ -31,7 +32,7 @@ use lemmy_structs::{ use lemmy_utils::{ location_info, settings::Settings, - utils::{check_slurs, check_slurs_opt}, + utils::{check_slurs, check_slurs_opt, is_valid_url_opt}, APIError, ConnectionId, LemmyError, @@ -155,9 +156,12 @@ impl Perform for CreateSite { // Make sure user is an admin is_admin(context.pool(), user.id).await?; + is_valid_url_opt(&data.icon)?; + is_valid_url_opt(&data.banner)?; + let site_form = SiteForm { - name: data.name.to_owned(), - description: data.description.to_owned(), + name: clean_text(&data.name), + description: data.description.as_ref().map(|d| clean_text(d)), icon: Some(data.icon.to_owned()), banner: Some(data.banner.to_owned()), creator_id: user.id, @@ -197,12 +201,15 @@ impl Perform for EditSite { let found_site = blocking(context.pool(), move |conn| Site::read(conn, 1)).await??; + is_valid_url_opt(&data.icon)?; + is_valid_url_opt(&data.banner)?; + let icon = diesel_option_overwrite(&data.icon); let banner = diesel_option_overwrite(&data.banner); let site_form = SiteForm { - name: data.name.to_owned(), - description: data.description.to_owned(), + name: clean_text(&data.name.to_owned()), + description: data.description.as_ref().map(|d| clean_text(d)), icon, banner, creator_id: found_site.creator_id, diff --git a/src/api/user.rs b/src/api/user.rs index 630f816de..7bc1a95f2 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -5,6 +5,7 @@ use crate::{ LemmyContext, }; use actix_web::web::Data; +use ammonia::clean_text; use anyhow::Context; use bcrypt::verify; use captcha::{gen, Difficulty}; @@ -55,6 +56,7 @@ use lemmy_utils::{ check_slurs, generate_random_string, is_valid_preferred_username, + is_valid_url_opt, is_valid_username, naive_from_unix, remove_slurs, @@ -160,11 +162,12 @@ impl Perform for Register { if !is_valid_username(&data.username) { return Err(APIError::err("invalid_username").into()); } + let email = data.email.as_ref().map(|e| clean_text(e)); // Register the new user let user_form = UserForm { - name: data.username.to_owned(), - email: Some(data.email.to_owned()), + name: clean_text(&data.username), + email: Some(email), matrix_user_id: None, avatar: None, banner: None, @@ -217,7 +220,7 @@ impl Perform for Register { Err(_e) => { let default_community_name = "main"; let community_form = CommunityForm { - name: default_community_name.to_string(), + name: clean_text(default_community_name), title: "The Default Community".to_string(), description: Some("The Default Community".to_string()), category_id: 1, @@ -340,7 +343,7 @@ impl Perform for SaveUserSettings { let bio = match &data.bio { Some(bio) => { if bio.chars().count() <= 300 { - Some(bio.to_owned()) + Some(clean_text(&bio.to_owned())) } else { return Err(APIError::err("bio_length_overflow").into()); } @@ -350,7 +353,11 @@ impl Perform for SaveUserSettings { let avatar = diesel_option_overwrite(&data.avatar); let banner = diesel_option_overwrite(&data.banner); - let email = diesel_option_overwrite(&data.email); + let email = data.email.as_ref().map(|e| clean_text(e)); + let email = diesel_option_overwrite(&email); + + is_valid_url_opt(&data.avatar)?; + is_valid_url_opt(&data.banner)?; // The DB constraint should stop too many characters let preferred_username = match &data.preferred_username { @@ -397,9 +404,9 @@ impl Perform for SaveUserSettings { }; let user_form = UserForm { - name: read_user.name, + name: clean_text(&read_user.name), email, - matrix_user_id: data.matrix_user_id.to_owned(), + matrix_user_id: data.matrix_user_id.as_ref().map(|m| clean_text(m)), avatar, banner, password_encrypted, @@ -654,7 +661,7 @@ impl Perform for BanUser { let form = ModBanForm { mod_user_id: user.id, other_user_id: data.user_id, - reason: data.reason.to_owned(), + reason: data.reason.as_ref().map(|r| clean_text(r)), banned: Some(data.ban), expires, }; @@ -983,7 +990,7 @@ impl Perform for CreatePrivateMessage { let content_slurs_removed = remove_slurs(&data.content.to_owned()); let private_message_form = PrivateMessageForm { - content: content_slurs_removed.to_owned(), + content: clean_text(&content_slurs_removed), creator_id: user.id, recipient_id: data.recipient_id, deleted: None,