Disallow <code> tags in bio

This commit is contained in:
silverpill 2022-10-06 21:16:52 +00:00
parent 448b5afa88
commit 0548e6e72a
5 changed files with 59 additions and 37 deletions

View file

@ -3,3 +3,4 @@ pub mod helpers;
pub mod mentions; pub mod mentions;
pub mod queries; pub mod queries;
pub mod types; pub mod types;
mod validators;

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_strict; use super::validators::clean_content;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Visibility { pub enum Visibility {
@ -263,15 +263,7 @@ impl PostCreateData {
/// Validate and clean post data (only for local posts). /// Validate and clean post data (only for local posts).
pub fn clean(&mut self, character_limit: usize) -> Result<(), ValidationError> { pub fn clean(&mut self, character_limit: usize) -> Result<(), ValidationError> {
assert!(self.object_id.is_none()); assert!(self.object_id.is_none());
if self.content.chars().count() > character_limit { self.content = clean_content(&self.content, character_limit)?;
return Err(ValidationError("post is too long"));
};
let content_safe = clean_html_strict(&self.content);
let content_trimmed = content_safe.trim();
if content_trimmed.is_empty() {
return Err(ValidationError("post can not be empty"));
};
self.content = content_trimmed.to_string();
Ok(()) Ok(())
} }
} }
@ -288,16 +280,7 @@ mod tests {
const POST_CHARACTER_LIMIT: usize = 1000; const POST_CHARACTER_LIMIT: usize = 1000;
#[test] #[test]
fn test_post_data_empty() { fn test_post_data_clean() {
let mut post_data_1 = PostCreateData {
content: " ".to_string(),
..Default::default()
};
assert_eq!(post_data_1.clean(POST_CHARACTER_LIMIT).is_ok(), false);
}
#[test]
fn test_post_data_trimming() {
let mut post_data_2 = PostCreateData { let mut post_data_2 = PostCreateData {
content: "test ".to_string(), content: "test ".to_string(),
..Default::default() ..Default::default()

View file

@ -0,0 +1,40 @@
use crate::errors::ValidationError;
use crate::utils::html::clean_html_strict;
const CONTENT_ALLOWED_TAGS: [&str; 4] = ["a", "br", "pre", "code"];
pub fn clean_content(
content: &str,
character_limit: usize,
) -> Result<String, ValidationError> {
if content.chars().count() > character_limit {
return Err(ValidationError("post is too long"));
};
let content_safe = clean_html_strict(content, &CONTENT_ALLOWED_TAGS);
let content_trimmed = content_safe.trim();
if content_trimmed.is_empty() {
return Err(ValidationError("post can not be empty"));
};
Ok(content_trimmed.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const POST_CHARACTER_LIMIT: usize = 1000;
#[test]
fn test_clean_content_empty() {
let content = " ";
let result = clean_content(content, POST_CHARACTER_LIMIT);
assert_eq!(result.is_ok(), false);
}
#[test]
fn test_clean_content_trimming() {
let content = "test ";
let cleaned = clean_content(content, POST_CHARACTER_LIMIT).unwrap();
assert_eq!(cleaned, "test");
}
}

View file

@ -4,6 +4,11 @@ use crate::utils::html::{clean_html, clean_html_strict};
use super::types::ExtraField; use super::types::ExtraField;
const USERNAME_RE: &str = r"^[a-zA-Z0-9_\.-]+$"; const USERNAME_RE: &str = r"^[a-zA-Z0-9_\.-]+$";
const DISPLAY_NAME_MAX_LENGTH: usize = 200;
const BIO_MAX_LENGTH: usize = 10000;
const BIO_ALLOWED_TAGS: [&str; 2] = ["a", "br"];
const FIELD_NAME_MAX_SIZE: usize = 500;
const FIELD_VALUE_MAX_SIZE: usize = 5000;
pub fn validate_username(username: &str) -> Result<(), ValidationError> { pub fn validate_username(username: &str) -> Result<(), ValidationError> {
if username.is_empty() { if username.is_empty() {
@ -19,8 +24,6 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> {
Ok(()) Ok(())
} }
const DISPLAY_NAME_MAX_LENGTH: usize = 200;
pub fn validate_display_name(display_name: &str) pub fn validate_display_name(display_name: &str)
-> Result<(), ValidationError> -> Result<(), ValidationError>
{ {
@ -30,8 +33,6 @@ pub fn validate_display_name(display_name: &str)
Ok(()) Ok(())
} }
const BIO_MAX_LENGTH: usize = 10000;
pub fn clean_bio(bio: &str, is_remote: bool) -> Result<String, ValidationError> { pub fn clean_bio(bio: &str, is_remote: bool) -> Result<String, ValidationError> {
let cleaned_bio = if is_remote { let cleaned_bio = if is_remote {
// Remote profile // Remote profile
@ -42,14 +43,11 @@ pub fn clean_bio(bio: &str, is_remote: bool) -> Result<String, ValidationError>
if bio.chars().count() > BIO_MAX_LENGTH { if bio.chars().count() > BIO_MAX_LENGTH {
return Err(ValidationError("bio is too long")); return Err(ValidationError("bio is too long"));
}; };
clean_html_strict(bio) clean_html_strict(bio, &BIO_ALLOWED_TAGS)
}; };
Ok(cleaned_bio) Ok(cleaned_bio)
} }
const FIELD_NAME_MAX_SIZE: usize = 500;
const FIELD_VALUE_MAX_SIZE: usize = 5000;
/// Validates extra fields and removes fields with empty labels /// Validates extra fields and removes fields with empty labels
pub fn clean_extra_fields(extra_fields: &[ExtraField]) pub fn clean_extra_fields(extra_fields: &[ExtraField])
-> Result<Vec<ExtraField>, ValidationError> -> Result<Vec<ExtraField>, ValidationError>
@ -57,7 +55,7 @@ pub fn clean_extra_fields(extra_fields: &[ExtraField])
let mut cleaned_extra_fields = vec![]; let mut cleaned_extra_fields = vec![];
for mut field in extra_fields.iter().cloned() { for mut field in extra_fields.iter().cloned() {
field.name = field.name.trim().to_string(); field.name = field.name.trim().to_string();
field.value = clean_html_strict(&field.value); field.value = clean_html_strict(&field.value, &BIO_ALLOWED_TAGS);
if field.name.is_empty() { if field.name.is_empty() {
continue; continue;
}; };

View file

@ -1,4 +1,5 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::iter::FromIterator;
use ammonia::Builder; use ammonia::Builder;
@ -12,13 +13,12 @@ pub fn clean_html(unsafe_html: &str) -> String {
safe_html safe_html
} }
pub fn clean_html_strict(unsafe_html: &str) -> String { pub fn clean_html_strict(
let mut allowed_tags = HashSet::new(); unsafe_html: &str,
allowed_tags.insert("a"); allowed_tags: &[&str],
allowed_tags.insert("br"); ) -> String {
allowed_tags.insert("pre"); let allowed_tags =
allowed_tags.insert("code"); HashSet::from_iter(allowed_tags.iter().copied());
let safe_html = Builder::default() let safe_html = Builder::default()
.tags(allowed_tags) .tags(allowed_tags)
.clean(unsafe_html) .clean(unsafe_html)
@ -47,7 +47,7 @@ mod tests {
#[test] #[test]
fn test_clean_html_strict() { 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 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); let safe_html = clean_html_strict(unsafe_html, &["a", "br", "code"]);
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>"#);
} }