Validate content of incoming Note objects

This commit is contained in:
silverpill 2022-02-08 21:33:05 +00:00
parent 2747e7b174
commit 6fc319f7dd
5 changed files with 39 additions and 8 deletions

View file

@ -337,7 +337,9 @@ paths:
type: object type: object
properties: properties:
status: status:
description: Text content of the post. description: |
Text content of the post.
Allowed HTML tags: a, br, pre, code.
type: string type: string
'media_ids[]': 'media_ids[]':
description: Array of Attachment ids to be attached as media. description: Array of Attachment ids to be attached as media.

View file

@ -37,6 +37,7 @@ use crate::models::relationships::queries::{
unfollow, unfollow,
}; };
use crate::models::users::queries::get_user_by_name; use crate::models::users::queries::get_user_by_name;
use crate::utils::html::clean_html;
use super::activity::{Object, Activity, create_activity_accept_follow}; use super::activity::{Object, Activity, create_activity_accept_follow};
use super::actor::Actor; use super::actor::Actor;
use super::constants::AP_PUBLIC; use super::constants::AP_PUBLIC;
@ -132,6 +133,16 @@ fn get_object_id(object: Value) -> Result<String, ValidationError> {
Ok(object_id) Ok(object_id)
} }
const CONTENT_MAX_SIZE: usize = 100000;
fn clean_note_content(content: &str) -> Result<String, ValidationError> {
if content.len() > CONTENT_MAX_SIZE {
return Err(ValidationError("content is too long"));
};
let content_safe = clean_html(content);
Ok(content_safe)
}
pub async fn process_note( pub async fn process_note(
config: &Config, config: &Config,
db_client: &mut impl GenericClient, db_client: &mut impl GenericClient,
@ -221,6 +232,7 @@ pub async fn process_note(
).await?; ).await?;
let content = object.content let content = object.content
.ok_or(ValidationError("no content"))?; .ok_or(ValidationError("no content"))?;
let content_cleaned = clean_note_content(&content)?;
let mut attachments: Vec<Uuid> = Vec::new(); let mut attachments: Vec<Uuid> = Vec::new();
if let Some(list) = object.attachment { if let Some(list) = object.attachment {
let mut downloaded = vec![]; let mut downloaded = vec![];
@ -375,7 +387,7 @@ pub async fn process_note(
Visibility::Direct Visibility::Direct
}; };
let post_data = PostCreateData { let post_data = PostCreateData {
content, content: content_cleaned,
in_reply_to_id, in_reply_to_id,
repost_of_id: None, repost_of_id: None,
visibility, visibility,

View file

@ -10,7 +10,7 @@ use crate::database::int_enum::{int_enum_from_sql, int_enum_to_sql};
use crate::errors::{ConversionError, DatabaseError, ValidationError}; use crate::errors::{ConversionError, DatabaseError, ValidationError};
use crate::models::attachments::types::DbMediaAttachment; use crate::models::attachments::types::DbMediaAttachment;
use crate::models::profiles::types::DbActorProfile; use crate::models::profiles::types::DbActorProfile;
use crate::utils::html::clean_html; use crate::utils::html::clean_html_strict;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Visibility { pub enum Visibility {
@ -220,7 +220,7 @@ impl PostCreateData {
if self.content.chars().count() > character_limit { if self.content.chars().count() > character_limit {
return Err(ValidationError("post is too long")); return Err(ValidationError("post is too long"));
}; };
let content_safe = clean_html(&self.content); let content_safe = clean_html_strict(&self.content);
let content_trimmed = content_safe.trim(); let content_trimmed = content_safe.trim();
if content_trimmed.is_empty() { if content_trimmed.is_empty() {
return Err(ValidationError("post can not be empty")); return Err(ValidationError("post can not be empty"));

View file

@ -11,7 +11,7 @@ use uuid::Uuid;
use crate::activitypub::actor::Actor; use crate::activitypub::actor::Actor;
use crate::activitypub::views::get_actor_url; use crate::activitypub::views::get_actor_url;
use crate::errors::ValidationError; use crate::errors::ValidationError;
use crate::utils::html::clean_html; use crate::utils::html::clean_html_strict;
use super::validators::{ use super::validators::{
validate_username, validate_username,
validate_display_name, validate_display_name,
@ -173,12 +173,12 @@ pub struct ProfileUpdateData {
impl ProfileUpdateData { impl ProfileUpdateData {
pub fn clean(&mut self) -> Result<(), ValidationError> { pub fn clean(&mut self) -> Result<(), ValidationError> {
// Validate and clean bio // Validate and clean bio
self.bio = self.bio.as_ref().map(|val| clean_html(val)); self.bio = self.bio.as_ref().map(|val| clean_html_strict(val));
// Clean extra fields and remove fields with empty labels // Clean extra fields and remove fields with empty labels
self.extra_fields = self.extra_fields.iter().cloned() self.extra_fields = self.extra_fields.iter().cloned()
.map(|mut field| { .map(|mut field| {
field.name = field.name.trim().to_string(); field.name = field.name.trim().to_string();
field.value = clean_html(&field.value); field.value = clean_html_strict(&field.value);
field field
}) })
.filter(|field| !field.name.is_empty()) .filter(|field| !field.name.is_empty())

View file

@ -3,6 +3,16 @@ use std::collections::HashSet;
use ammonia::Builder; use ammonia::Builder;
pub fn clean_html(unsafe_html: &str) -> String { pub fn clean_html(unsafe_html: &str) -> String {
let safe_html = Builder::default()
.add_generic_attributes(&["class"])
.add_tag_attributes("a", &["rel", "target"])
.link_rel(None)
.clean(unsafe_html)
.to_string();
safe_html
}
pub fn clean_html_strict(unsafe_html: &str) -> String {
let mut allowed_tags = HashSet::new(); let mut allowed_tags = HashSet::new();
allowed_tags.insert("a"); allowed_tags.insert("a");
allowed_tags.insert("br"); allowed_tags.insert("br");
@ -22,8 +32,15 @@ mod tests {
#[test] #[test]
fn test_clean_html() { fn test_clean_html() {
let unsafe_html = r#"<p>test <b>bold</b><script>dangerous</script> with <a href="https://example.com">link</a> and <code>code</code></p>"#; let unsafe_html = r#"<p><span class="h-card"><a href="https://example.com/user" class="u-url mention" rel="ugc">@<span>user</span></a></span> test</p>"#;
let safe_html = clean_html(unsafe_html); let safe_html = clean_html(unsafe_html);
assert_eq!(safe_html, r#"<p><span class="h-card"><a href="https://example.com/user" class="u-url mention" rel="ugc">@<span>user</span></a></span> test</p>"#);
}
#[test]
fn test_clean_html_strict() {
let unsafe_html = r#"<p>test <b>bold</b><script>dangerous</script> with <a href="https://example.com">link</a> and <code>code</code></p>"#;
let safe_html = clean_html_strict(unsafe_html);
assert_eq!(safe_html, r#"test bold with <a href="https://example.com" rel="noopener noreferrer">link</a> and <code>code</code>"#); assert_eq!(safe_html, r#"test bold with <a href="https://example.com" rel="noopener noreferrer">link</a> and <code>code</code>"#);
} }
} }