Only sanitize strings when generating RSS feeds and emails (fixes #4003) (#4024)

* Only sanitize strings when generating RSS feeds and emails (fixes #4003)

* clippy

* fix test
This commit is contained in:
Nutomic 2023-10-11 16:48:19 +02:00 committed by GitHub
parent 6d7b38f4de
commit 291ff19718
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 140 additions and 273 deletions

View file

@ -554,29 +554,3 @@ test("Report a post", async () => {
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
expect(betaReport.reason).toBe(alphaReport.reason);
});
test("Sanitize HTML", async () => {
let betaCommunity = (await resolveBetaCommunity(beta)).community;
if (!betaCommunity) {
throw "Missing beta community";
}
let name = randomString(5);
let body = "<script>alert('xss');</script> hello &\"'";
let form: CreatePost = {
name,
body,
community_id: betaCommunity.community.id,
};
let post = await beta.createPost(form);
// first escaping for the api
expect(post.post_view.post.body).toBe(
"&lt;script>alert(&#x27;xss&#x27;);&lt;/script> hello &amp;&quot;&#x27;",
);
let alphaPost = (await resolvePost(alpha, post.post_view.post)).post;
// second escaping over federation, avoid double escape of &
expect(alphaPost?.post.body).toBe(
"&lt;script>alert(&#x27;xss&#x27;);&lt;/script> hello &amp;&quot;&#x27;",
);
});

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, sanitize_html_api, send_new_report_email_to_admins},
utils::{check_community_ban, send_new_report_email_to_admins},
};
use lemmy_db_schema::{
source::{
@ -26,7 +26,7 @@ pub async fn create_comment_report(
) -> Result<Json<CommentReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = sanitize_html_api(data.reason.trim());
let reason = data.reason.trim().to_string();
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;

View file

@ -4,7 +4,7 @@ use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{is_mod_or_admin, remove_user_data_in_community, sanitize_html_api_opt},
utils::{is_mod_or_admin, remove_user_data_in_community},
};
use lemmy_db_schema::{
source::{
@ -81,7 +81,7 @@ pub async fn ban_from_community(
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
reason: sanitize_html_api_opt(&data.reason),
reason: data.reason.clone(),
banned: Some(data.ban),
expires,
};

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
community::{CommunityResponse, HideCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{is_admin, sanitize_html_api_opt},
utils::is_admin,
};
use lemmy_db_schema::{
source::{
@ -34,7 +34,7 @@ pub async fn hide_community(
let mod_hide_community_form = ModHideCommunityForm {
community_id: data.community_id,
mod_person_id: local_user_view.person.id,
reason: sanitize_html_api_opt(&data.reason),
reason: data.reason.clone(),
hidden: Some(data.hidden),
};

View file

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
person::{BanPerson, BanPersonResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{is_admin, remove_user_data, sanitize_html_api_opt},
utils::{is_admin, remove_user_data},
};
use lemmy_db_schema::{
source::{
@ -61,7 +61,7 @@ pub async fn ban_from_site(
let form = ModBanForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
reason: sanitize_html_api_opt(&data.reason),
reason: data.reason.clone(),
banned: Some(data.ban),
expires,
};

View file

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::SaveUserSettings,
utils::{sanitize_html_api_opt, send_verification_email},
utils::send_verification_email,
SuccessResponse,
};
use lemmy_db_schema::{
@ -28,13 +28,10 @@ pub async fn save_user_settings(
) -> Result<Json<SuccessResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let bio = sanitize_html_api_opt(&data.bio);
let display_name = sanitize_html_api_opt(&data.display_name);
let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let bio = diesel_option_overwrite(bio);
let display_name = diesel_option_overwrite(display_name);
let bio = diesel_option_overwrite(data.bio.clone());
let display_name = diesel_option_overwrite(data.display_name.clone());
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
let email_deref = data.email.as_deref().map(str::to_lowercase);
let email = diesel_option_overwrite(email_deref.clone());
@ -82,7 +79,6 @@ pub async fn save_user_settings(
let person_id = local_user_view.person.id;
let default_listing_type = data.default_listing_type;
let default_sort_type = data.default_sort_type;
let theme = sanitize_html_api_opt(&data.theme);
let person_form = PersonUpdateForm {
display_name,
@ -114,7 +110,7 @@ pub async fn save_user_settings(
show_scores: data.show_scores,
default_sort_type,
default_listing_type,
theme,
theme: data.theme.clone(),
interface_language: data.interface_language.clone(),
open_links_in_new_tab: data.open_links_in_new_tab,
infinite_scroll_enabled: data.infinite_scroll_enabled,

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostReport, PostReportResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, sanitize_html_api, send_new_report_email_to_admins},
utils::{check_community_ban, send_new_report_email_to_admins},
};
use lemmy_db_schema::{
source::{
@ -26,7 +26,7 @@ pub async fn create_post_report(
) -> Result<Json<PostReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = sanitize_html_api(data.reason.trim());
let reason = data.reason.trim().to_string();
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;

View file

@ -3,7 +3,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse},
utils::{sanitize_html_api, send_new_report_email_to_admins},
utils::send_new_report_email_to_admins,
};
use lemmy_db_schema::{
source::{
@ -24,7 +24,7 @@ pub async fn create_pm_report(
) -> Result<Json<PrivateMessageReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = sanitize_html_api(data.reason.trim());
let reason = data.reason.trim().to_string();
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;
@ -35,7 +35,7 @@ pub async fn create_pm_report(
creator_id: person_id,
private_message_id,
original_pm_text: private_message.content,
reason: reason.clone(),
reason,
};
let report = PrivateMessageReport::report(&mut context.pool(), &report_form)

View file

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{PurgeComment, PurgeItemResponse},
utils::{is_admin, sanitize_html_api_opt},
utils::is_admin,
};
use lemmy_db_schema::{
source::{
@ -35,10 +35,9 @@ pub async fn purge_comment(
Comment::delete(&mut context.pool(), comment_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgeCommentForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
post_id,
};

View file

@ -3,7 +3,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeCommunity, PurgeItemResponse},
utils::{is_admin, purge_image_posts_for_community, sanitize_html_api_opt},
utils::{is_admin, purge_image_posts_for_community},
};
use lemmy_db_schema::{
source::{
@ -42,10 +42,9 @@ pub async fn purge_community(
Community::delete(&mut context.pool(), community_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgeCommunityForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
};
AdminPurgeCommunity::create(&mut context.pool(), &form).await?;

View file

@ -3,7 +3,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::delete_image_from_pictrs,
site::{PurgeItemResponse, PurgePerson},
utils::{is_admin, sanitize_html_api_opt},
utils::is_admin,
};
use lemmy_db_schema::{
source::{
@ -41,10 +41,9 @@ pub async fn purge_person(
Person::delete(&mut context.pool(), person_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgePersonForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
};
AdminPurgePerson::create(&mut context.pool(), &form).await?;

View file

@ -3,7 +3,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeItemResponse, PurgePost},
utils::{is_admin, sanitize_html_api_opt},
utils::is_admin,
};
use lemmy_db_schema::{
source::{
@ -43,10 +43,9 @@ pub async fn purge_post(
Post::delete(&mut context.pool(), post_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgePostForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
community_id,
};

View file

@ -20,7 +20,10 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::{CommentView, LocalUserView, PostView};
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{error::LemmyError, utils::mention::MentionData};
use lemmy_utils::{
error::LemmyError,
utils::{markdown::markdown_to_html, mention::MentionData},
};
pub async fn build_comment_response(
context: &LemmyContext,
@ -121,10 +124,11 @@ pub async fn send_local_notifs(
// Send an email to those local users that have notifications on
if do_send_email {
let lang = get_interface_language(&mention_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&mention_user_view,
&lang.notification_mentioned_by_subject(&person.name),
&lang.notification_mentioned_by_body(&comment.content, &inbox_link, &person.name),
&lang.notification_mentioned_by_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
@ -164,10 +168,11 @@ pub async fn send_local_notifs(
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(&comment.content, &inbox_link, &person.name),
&lang.notification_comment_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
@ -201,10 +206,11 @@ pub async fn send_local_notifs(
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(&comment.content, &inbox_link, &person.name),
&lang.notification_post_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await

View file

@ -723,37 +723,6 @@ pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError
Ok(Url::parse(&format!("{community_id}/moderators"))?.into())
}
/// Replace special HTML characters in API parameters to prevent XSS attacks.
///
/// Taken from https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md#output-encoding-for-html-contexts
///
/// `>` is left in place because it is interpreted as markdown quote.
pub fn sanitize_html_api(data: &str) -> String {
data
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('\"', "&quot;")
.replace('\'', "&#x27;")
}
pub fn sanitize_html_api_opt(data: &Option<String>) -> Option<String> {
data.as_ref().map(|d| sanitize_html_api(d))
}
/// Replace special HTML characters in federation parameters to prevent XSS attacks.
///
/// Unlike [sanitize_html_api()] it leaves `&` in place to avoid double escaping.
pub fn sanitize_html_federation(data: &str) -> String {
data
.replace('<', "&lt;")
.replace('\"', "&quot;")
.replace('\'', "&#x27;")
}
pub fn sanitize_html_federation_opt(data: &Option<String>) -> Option<String> {
data.as_ref().map(|d| sanitize_html_federation(d))
}
pub fn create_login_cookie(jwt: Sensitive<String>) -> Cookie<'static> {
let mut cookie = Cookie::new(AUTH_COOKIE_NAME, jwt.into_inner());
cookie.set_secure(true);

View file

@ -12,7 +12,6 @@ use lemmy_api_common::{
generate_local_apub_endpoint,
get_post,
local_site_to_slur_regex,
sanitize_html_api,
EndpointType,
},
};
@ -52,7 +51,6 @@ pub async fn create_comment(
&local_site_to_slur_regex(&local_site),
);
is_valid_body_field(&Some(content.clone()), false)?;
let content = sanitize_html_api(&content);
// Check for a community ban
let post_id = data.post_id;

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, EditComment},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, local_site_to_slur_regex, sanitize_html_api_opt},
utils::{check_community_ban, local_site_to_slur_regex},
};
use lemmy_db_schema::{
source::{
@ -63,7 +63,6 @@ pub async fn update_comment(
.as_ref()
.map(|c| remove_slurs(c, &local_site_to_slur_regex(&local_site)));
is_valid_body_field(&content, false)?;
let content = sanitize_html_api_opt(&content);
let comment_id = data.comment_id;
let form = CommentUpdateForm {

View file

@ -11,8 +11,6 @@ use lemmy_api_common::{
generate_shared_inbox_url,
is_admin,
local_site_to_slur_regex,
sanitize_html_api,
sanitize_html_api_opt,
EndpointType,
},
};
@ -57,14 +55,10 @@ pub async fn create_community(
let icon = diesel_option_overwrite_to_url_create(&data.icon)?;
let banner = diesel_option_overwrite_to_url_create(&data.banner)?;
let name = sanitize_html_api(&data.name);
let title = sanitize_html_api(&data.title);
let description = sanitize_html_api_opt(&data.description);
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&name, &slur_regex)?;
check_slurs(&title, &slur_regex)?;
check_slurs_opt(&description, &slur_regex)?;
check_slurs(&data.name, &slur_regex)?;
check_slurs(&data.title, &slur_regex)?;
check_slurs_opt(&data.description, &slur_regex)?;
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
is_valid_body_field(&data.description, false)?;
@ -85,9 +79,9 @@ pub async fn create_community(
let keypair = generate_actor_keypair()?;
let community_form = CommunityInsertForm::builder()
.name(name)
.title(title)
.description(description)
.name(data.name.clone())
.title(data.title.clone())
.description(data.description.clone())
.icon(icon)
.banner(banner)
.nsfw(data.nsfw)

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
community::{CommunityResponse, EditCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_to_slur_regex, sanitize_html_api_opt},
utils::local_site_to_slur_regex,
};
use lemmy_db_schema::{
newtypes::PersonId,
@ -37,12 +37,9 @@ pub async fn update_community(
check_slurs_opt(&data.description, &slur_regex)?;
is_valid_body_field(&data.description, false)?;
let title = sanitize_html_api_opt(&data.title);
let description = sanitize_html_api_opt(&data.description);
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let description = diesel_option_overwrite(description);
let description = diesel_option_overwrite(data.description.clone());
// Verify its a mod (only mods can edit it)
let community_id = data.community_id;
@ -67,7 +64,7 @@ pub async fn update_community(
}
let community_form = CommunityUpdateForm {
title,
title: data.title.clone(),
description,
icon,
banner,

View file

@ -3,7 +3,7 @@ use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{CreateCustomEmoji, CustomEmojiResponse},
utils::{is_admin, sanitize_html_api},
utils::is_admin,
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
@ -23,15 +23,11 @@ pub async fn create_custom_emoji(
// Make sure user is an admin
is_admin(&local_user_view)?;
let shortcode = sanitize_html_api(data.shortcode.to_lowercase().trim());
let alt_text = sanitize_html_api(&data.alt_text);
let category = sanitize_html_api(&data.category);
let emoji_form = CustomEmojiInsertForm::builder()
.local_site_id(local_site.id)
.shortcode(shortcode)
.alt_text(alt_text)
.category(category)
.shortcode(data.shortcode.to_lowercase().trim().to_string())
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.image_url(data.clone().image_url.into())
.build();
let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;

View file

@ -3,7 +3,7 @@ use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{CustomEmojiResponse, EditCustomEmoji},
utils::{is_admin, sanitize_html_api},
utils::is_admin,
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
@ -23,13 +23,10 @@ pub async fn update_custom_emoji(
// Make sure user is an admin
is_admin(&local_user_view)?;
let alt_text = sanitize_html_api(&data.alt_text);
let category = sanitize_html_api(&data.category);
let emoji_form = CustomEmojiUpdateForm::builder()
.local_site_id(local_site.id)
.alt_text(alt_text)
.category(category)
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.image_url(data.clone().image_url.into())
.build();
let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;

View file

@ -13,8 +13,6 @@ use lemmy_api_common::{
honeypot_check,
local_site_to_slur_regex,
mark_post_as_read,
sanitize_html_api,
sanitize_html_api_opt,
EndpointType,
},
};
@ -92,11 +90,6 @@ pub async fn create_post(
.map(|u| (u.title, u.description, u.embed_video_url))
.unwrap_or_default();
let name = sanitize_html_api(data.name.trim());
let body = sanitize_html_api_opt(&data.body);
let embed_title = sanitize_html_api_opt(&embed_title);
let embed_description = sanitize_html_api_opt(&embed_description);
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(
@ -120,9 +113,9 @@ pub async fn create_post(
};
let post_form = PostInsertForm::builder()
.name(name)
.name(data.name.trim().to_string())
.url(url)
.body(body)
.body(data.body.clone())
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
post::{EditPost, PostResponse},
request::fetch_site_data,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, local_site_to_slur_regex, sanitize_html_api_opt},
utils::{check_community_ban, local_site_to_slur_regex},
};
use lemmy_db_schema::{
source::{
@ -75,12 +75,6 @@ pub async fn update_post(
.map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
.unwrap_or_default();
let name = sanitize_html_api_opt(&data.name);
let body = sanitize_html_api_opt(&data.body);
let body = diesel_option_overwrite(body);
let embed_title = embed_title.map(|e| sanitize_html_api_opt(&e));
let embed_description = embed_description.map(|e| sanitize_html_api_opt(&e));
let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
@ -90,9 +84,9 @@ pub async fn update_post(
.await?;
let post_form = PostUpdateForm {
name,
name: data.name.clone(),
url,
body,
body: diesel_option_overwrite(data.body.clone()),
nsfw: data.nsfw,
embed_title,
embed_description,

View file

@ -9,7 +9,6 @@ use lemmy_api_common::{
generate_local_apub_endpoint,
get_interface_language,
local_site_to_slur_regex,
sanitize_html_api,
send_email_to_user,
EndpointType,
},
@ -24,7 +23,7 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::{slurs::remove_slurs, validation::is_valid_body_field},
utils::{markdown::markdown_to_html, slurs::remove_slurs, validation::is_valid_body_field},
};
#[tracing::instrument(skip(context))]
@ -35,8 +34,7 @@ pub async fn create_private_message(
) -> Result<Json<PrivateMessageResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let content = sanitize_html_api(&data.content);
let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
let content = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content.clone()), false)?;
check_person_block(
@ -83,6 +81,7 @@ pub async fn create_private_message(
let lang = get_interface_language(&local_recipient);
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
let sender_name = &local_user_view.person.name;
let content = markdown_to_html(&content);
send_email_to_user(
&local_recipient,
&lang.notification_private_message_subject(sender_name),

View file

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_to_slur_regex, sanitize_html_api},
utils::local_site_to_slur_regex,
};
use lemmy_db_schema::{
source::{
@ -36,8 +36,7 @@ pub async fn update_private_message(
}
// Doing the update
let content = sanitize_html_api(&data.content);
let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
let content = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content.clone()), false)?;
let private_message_id = data.private_message_id;

View file

@ -4,13 +4,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{CreateSite, SiteResponse},
utils::{
generate_site_inbox_url,
is_admin,
local_site_rate_limit_to_rate_limit_config,
sanitize_html_api,
sanitize_html_api_opt,
},
utils::{generate_site_inbox_url, is_admin, local_site_rate_limit_to_rate_limit_config},
};
use lemmy_db_schema::{
newtypes::DbUrl,
@ -55,14 +49,11 @@ pub async fn create_site(
let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
let keypair = generate_actor_keypair()?;
let name = sanitize_html_api(&data.name);
let sidebar = sanitize_html_api_opt(&data.sidebar);
let description = sanitize_html_api_opt(&data.description);
let site_form = SiteUpdateForm {
name: Some(name),
sidebar: diesel_option_overwrite(sidebar),
description: diesel_option_overwrite(description),
name: Some(data.name.clone()),
sidebar: diesel_option_overwrite(data.sidebar.clone()),
description: diesel_option_overwrite(data.description.clone()),
icon: diesel_option_overwrite_to_url(&data.icon)?,
banner: diesel_option_overwrite_to_url(&data.banner)?,
actor_id: Some(actor_id),
@ -77,10 +68,6 @@ pub async fn create_site(
Site::update(&mut context.pool(), site_id, &site_form).await?;
let application_question = sanitize_html_api_opt(&data.application_question);
let default_theme = sanitize_html_api_opt(&data.default_theme);
let legal_information = sanitize_html_api_opt(&data.legal_information);
let local_site_form = LocalSiteUpdateForm {
// Set the site setup to true
site_setup: Some(true),
@ -89,11 +76,11 @@ pub async fn create_site(
enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification,
application_question: diesel_option_overwrite(application_question),
application_question: diesel_option_overwrite(data.application_question.clone()),
private_instance: data.private_instance,
default_theme,
default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type,
legal_information: diesel_option_overwrite(legal_information),
legal_information: diesel_option_overwrite(data.legal_information.clone()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),

View file

@ -3,7 +3,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{EditSite, SiteResponse},
utils::{is_admin, local_site_rate_limit_to_rate_limit_config, sanitize_html_api_opt},
utils::{is_admin, local_site_rate_limit_to_rate_limit_config},
};
use lemmy_db_schema::{
source::{
@ -54,14 +54,10 @@ pub async fn update_site(
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
}
let name = sanitize_html_api_opt(&data.name);
let sidebar = sanitize_html_api_opt(&data.sidebar);
let description = sanitize_html_api_opt(&data.description);
let site_form = SiteUpdateForm {
name,
sidebar: diesel_option_overwrite(sidebar),
description: diesel_option_overwrite(description),
name: data.name.clone(),
sidebar: diesel_option_overwrite(data.sidebar.clone()),
description: diesel_option_overwrite(data.description.clone()),
icon: diesel_option_overwrite_to_url(&data.icon)?,
banner: diesel_option_overwrite_to_url(&data.banner)?,
updated: Some(Some(naive_now())),
@ -74,21 +70,17 @@ pub async fn update_site(
// Diesel will throw an error for empty update forms
.ok();
let application_question = sanitize_html_api_opt(&data.application_question);
let default_theme = sanitize_html_api_opt(&data.default_theme);
let legal_information = sanitize_html_api_opt(&data.legal_information);
let local_site_form = LocalSiteUpdateForm {
enable_downvotes: data.enable_downvotes,
registration_mode: data.registration_mode,
enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification,
application_question: diesel_option_overwrite(application_question),
application_question: diesel_option_overwrite(data.application_question.clone()),
private_instance: data.private_instance,
default_theme,
default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type,
legal_information: diesel_option_overwrite(legal_information),
legal_information: diesel_option_overwrite(data.legal_information.clone()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),

View file

@ -12,8 +12,6 @@ use lemmy_api_common::{
honeypot_check,
local_site_to_slur_regex,
password_length_check,
sanitize_html_api,
sanitize_html_api_opt,
send_new_applicant_email_to_admins,
send_verification_email,
EndpointType,
@ -93,12 +91,6 @@ pub async fn register(
check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?;
if sanitize_html_api(&data.username) != data.username {
Err(LemmyErrorType::InvalidName)?;
}
let answer = sanitize_html_api_opt(&data.answer);
let actor_keypair = generate_actor_keypair()?;
is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?;
let actor_id = generate_local_apub_endpoint(
@ -154,7 +146,7 @@ pub async fn register(
let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id,
// We already made sure answer was not null above
answer: answer.expect("must have an answer"),
answer: data.answer.clone().expect("must have an answer"),
};
RegistrationApplication::create(&mut context.pool(), &form).await?;

View file

@ -23,7 +23,7 @@ use anyhow::anyhow;
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{remove_user_data, remove_user_data_in_community, sanitize_html_federation_opt},
utils::{remove_user_data, remove_user_data_in_community},
};
use lemmy_db_schema::{
source::{
@ -173,7 +173,7 @@ impl ActivityHandler for BlockUser {
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: sanitize_html_federation_opt(&self.summary),
reason: self.summary,
banned: Some(true),
expires,
};
@ -207,7 +207,7 @@ impl ActivityHandler for BlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: sanitize_html_federation_opt(&self.summary),
reason: self.summary,
banned: Some(true),
expires,
};

View file

@ -17,7 +17,7 @@ use activitypub_federation::{
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_federation_opt};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
@ -118,7 +118,7 @@ impl ActivityHandler for UndoBlockUser {
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: sanitize_html_federation_opt(&self.object.summary),
reason: self.object.summary,
banned: Some(false),
expires,
};
@ -137,7 +137,7 @@ impl ActivityHandler for UndoBlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: sanitize_html_federation_opt(&self.object.summary),
reason: self.object.summary,
banned: Some(false),
expires,
};

View file

@ -11,7 +11,7 @@ use activitypub_federation::{
kinds::activity::FlagType,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_federation};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
@ -90,7 +90,7 @@ impl ActivityHandler for Report {
post_id: post.id,
original_post_name: post.name.clone(),
original_post_url: post.url.clone(),
reason: sanitize_html_federation(&self.summary),
reason: self.summary.clone(),
original_post_body: post.body.clone(),
};
PostReport::report(&mut context.pool(), &report_form).await?;
@ -100,7 +100,7 @@ impl ActivityHandler for Report {
creator_id: actor.id,
comment_id: comment.id,
original_comment_text: comment.content.clone(),
reason: sanitize_html_federation(&self.summary),
reason: self.summary.clone(),
};
CommentReport::report(&mut context.pool(), &report_form).await?;
}

View file

@ -8,7 +8,7 @@ use crate::{
protocol::{activities::deletion::delete::Delete, IdOrNestedObject},
};
use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::ActivityHandler};
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_federation_opt};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
comment::{Comment, CommentUpdateForm},
@ -105,8 +105,6 @@ pub(in crate::activities) async fn receive_remove_action(
reason: Option<String>,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let reason = sanitize_html_federation_opt(&reason);
match DeletableObjects::read_from_db(object, context).await? {
DeletableObjects::Community(community) => {
if community.local {

View file

@ -16,10 +16,7 @@ use activitypub_federation::{
traits::Object,
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_federation},
};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm, CommentUpdateForm},
@ -162,7 +159,6 @@ impl Object for ApubComment {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let content = remove_slurs(&content, slur_regex);
let content = sanitize_html_federation(&content);
let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;

View file

@ -17,10 +17,7 @@ use activitypub_federation::{
traits::{Actor, Object},
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_federation_opt},
};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
@ -135,8 +132,6 @@ impl Object for ApubSite {
let instance = DbInstance::read_or_create(&mut data.pool(), domain.to_string()).await?;
let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source);
let sidebar = sanitize_html_federation_opt(&sidebar);
let description = sanitize_html_federation_opt(&apub.summary);
let site_form = SiteInsertForm {
name: apub.name.clone(),
@ -144,7 +139,7 @@ impl Object for ApubSite {
updated: apub.updated,
icon: apub.icon.clone().map(|i| i.url.into()),
banner: apub.image.clone().map(|i| i.url.into()),
description,
description: apub.summary,
actor_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(naive_now()),
inbox_url: Some(apub.inbox.clone().into()),

View file

@ -20,12 +20,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{
generate_outbox_url,
local_site_opt_to_slur_regex,
sanitize_html_federation,
sanitize_html_federation_opt,
},
utils::{generate_outbox_url, local_site_opt_to_slur_regex},
};
use lemmy_db_schema::{
source::{
@ -150,17 +145,14 @@ impl Object for ApubPerson {
) -> Result<ApubPerson, LemmyError> {
let instance_id = fetch_instance_actor_for_object(&person.id, context).await?;
let name = sanitize_html_federation(&person.preferred_username);
let display_name = sanitize_html_federation_opt(&person.name);
let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
let bio = sanitize_html_federation_opt(&bio);
// Some Mastodon users have `name: ""` (empty string), need to convert that to `None`
// https://github.com/mastodon/mastodon/issues/25233
let display_name = display_name.filter(|n| !n.is_empty());
let display_name = person.name.filter(|n| !n.is_empty());
let person_form = PersonInsertForm {
name,
name: person.preferred_username,
display_name,
banned: None,
ban_expires: None,
@ -275,7 +267,7 @@ pub(crate) mod tests {
assert_eq!(person.name, "lanodan");
assert!(!person.local);
assert_eq!(context.request_count(), 0);
assert_eq!(person.bio.as_ref().unwrap().len(), 878);
assert_eq!(person.bio.as_ref().unwrap().len(), 873);
cleanup((person, site), &context).await;
}

View file

@ -25,13 +25,7 @@ use html2md::parse_html;
use lemmy_api_common::{
context::LemmyContext,
request::fetch_site_data,
utils::{
is_mod_or_admin,
local_site_opt_to_sensitive,
local_site_opt_to_slur_regex,
sanitize_html_federation,
sanitize_html_federation_opt,
},
utils::{is_mod_or_admin, local_site_opt_to_sensitive, local_site_opt_to_slur_regex},
};
use lemmy_db_schema::{
self,
@ -231,17 +225,11 @@ impl Object for ApubPost {
.unwrap_or_default();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let body_slurs_removed =
read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
.map(|s| remove_slurs(&s, slur_regex));
let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
.map(|s| remove_slurs(&s, slur_regex));
let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
let name = sanitize_html_federation(&name);
let body = sanitize_html_federation_opt(&body_slurs_removed);
let embed_title = sanitize_html_federation_opt(&embed_title);
let embed_description = sanitize_html_federation_opt(&embed_description);
PostInsertForm {
name,
url: url.map(Into::into),

View file

@ -12,10 +12,7 @@ use activitypub_federation::{
traits::Object,
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{check_person_block, sanitize_html_federation},
};
use lemmy_api_common::{context::LemmyContext, utils::check_person_block};
use lemmy_db_schema::{
source::{
person::Person,
@ -125,7 +122,6 @@ impl Object for ApubPrivateMessage {
check_person_block(creator.id, recipient.id, &mut context.pool()).await?;
let content = read_from_string_or_source(&note.content, &None, &note.source);
let content = sanitize_html_federation(&content);
let form = PrivateMessageInsertForm {
creator_id: creator.id,

View file

@ -23,10 +23,7 @@ use activitypub_federation::{
},
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_federation, sanitize_html_federation_opt},
};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
newtypes::InstanceId,
source::community::{CommunityInsertForm, CommunityUpdateForm},
@ -97,14 +94,11 @@ impl Group {
}
pub(crate) fn into_insert_form(self, instance_id: InstanceId) -> CommunityInsertForm {
let name = sanitize_html_federation(&self.preferred_username);
let title = sanitize_html_federation(&self.name.unwrap_or(self.preferred_username));
let description = read_from_string_or_source_opt(&self.summary, &None, &self.source);
let description = sanitize_html_federation_opt(&description);
CommunityInsertForm {
name,
title,
name: self.preferred_username.clone(),
title: self.name.unwrap_or(self.preferred_username.clone()),
description,
removed: None,
published: self.published,

View file

@ -22,7 +22,7 @@ use lemmy_db_views_actor::{
use lemmy_utils::{
cache_header::cache_1hour,
error::LemmyError,
utils::markdown::markdown_to_html,
utils::markdown::{markdown_to_html, sanitize_html},
};
use once_cell::sync::Lazy;
use rss::{
@ -289,7 +289,7 @@ async fn get_feed_community(
.items(items);
if let Some(community_desc) = community.description {
channel_builder.description(&community_desc);
channel_builder.description(markdown_to_html(&community_desc));
}
Ok(channel_builder)
@ -328,7 +328,7 @@ async fn get_feed_front(
.items(items);
if let Some(site_desc) = site_view.site.description {
channel_builder.description(&site_desc);
channel_builder.description(markdown_to_html(&site_desc));
}
Ok(channel_builder)
@ -457,7 +457,7 @@ fn create_post_items(
let mut i = ItemBuilder::default();
let mut dc_extension = DublinCoreExtensionBuilder::default();
i.title(p.post.name);
i.title(sanitize_html(&p.post.name));
dc_extension.creators(vec![p.creator.actor_id.to_string()]);
@ -472,14 +472,18 @@ fn create_post_items(
.build();
i.guid(guid);
let community_url = format!("{}/c/{}", protocol_and_hostname, p.community.name);
let community_url = format!(
"{}/c/{}",
protocol_and_hostname,
sanitize_html(&p.community.name)
);
// TODO add images
let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
p.creator.actor_id,
p.creator.name,
sanitize_html(&p.creator.name),
community_url,
p.community.name,
sanitize_html(&p.community.name),
p.counts.score,
post_url,
p.counts.comments);

View file

@ -12,6 +12,20 @@ static MARKDOWN_PARSER: Lazy<MarkdownIt> = Lazy::new(|| {
parser
});
/// Replace special HTML characters in API parameters to prevent XSS attacks.
///
/// Taken from https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md#output-encoding-for-html-contexts
///
/// `>` is left in place because it is interpreted as markdown quote.
pub fn sanitize_html(text: &str) -> String {
text
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('\"', "&quot;")
.replace('\'', "&#x27;")
}
/// Converts text from markdown to HTML, while escaping special characters.
pub fn markdown_to_html(text: &str) -> String {
MARKDOWN_PARSER.parse(text).xrender()
}
@ -21,7 +35,7 @@ mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::markdown::markdown_to_html;
use super::*;
#[test]
fn test_basic_markdown() {
@ -71,6 +85,11 @@ mod tests {
"::: spoiler click to see more\nhow spicy!\n:::\n",
"<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n"
),
(
"escape html special chars",
"<script>alert('xss');</script> hello &\"",
"<p>&lt;script&gt;alert(xss);&lt;/script&gt; hello &amp;&quot;</p>\n"
)
];
tests.iter().for_each(|&(msg, input, expected)| {
@ -83,4 +102,11 @@ mod tests {
);
});
}
#[test]
fn test_sanitize_html() {
let sanitized = sanitize_html("<script>alert('xss');</script> hello &\"'");
let expected = "&lt;script>alert(&#x27;xss&#x27;);&lt;/script> hello &amp;&quot;&#x27;";
assert_eq!(expected, sanitized)
}
}

@ -1 +1 @@
Subproject commit 18da10858d8c63750beb06247947f25d91944741
Subproject commit e943f97fe481dc425acdebc8872bf1fdcabaf875