diff --git a/Cargo.lock b/Cargo.lock index e9d5ac5d6f..db4c2ac1cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,19 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ammonia" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + [[package]] name = "analytics" version = "0.1.0" @@ -1737,7 +1750,7 @@ checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" dependencies = [ "chrono", "chrono-tz-build", - "phf", + "phf 0.11.2", ] [[package]] @@ -1747,8 +1760,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" dependencies = [ "parse-zoneinfo", - "phf", - "phf_codegen", + "phf 0.11.2", + "phf_codegen 0.11.2", ] [[package]] @@ -1878,6 +1891,7 @@ dependencies = [ name = "common_utils" version = "0.1.0" dependencies = [ + "ammonia", "async-trait", "base64 0.22.1", "base64-serde", @@ -1921,6 +1935,7 @@ dependencies = [ "time", "tokio 1.45.1", "url", + "urlencoding", "utoipa", "uuid", ] @@ -3282,6 +3297,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.1.31" @@ -3784,6 +3809,20 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "http" version = "0.2.12" @@ -4845,6 +4884,32 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "masking" version = "0.1.0" @@ -5159,6 +5224,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -5838,13 +5909,32 @@ dependencies = [ "indexmap 2.9.0", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", ] [[package]] @@ -5853,8 +5943,18 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", ] [[package]] @@ -5863,10 +5963,19 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", "rand 0.8.5", ] +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + [[package]] name = "phf_shared" version = "0.11.2" @@ -6049,6 +6158,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.20" @@ -8238,6 +8353,31 @@ dependencies = [ "tokio 1.45.1", ] +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot 0.12.3", + "phf_shared 0.11.2", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", +] + [[package]] name = "stringmatch" version = "0.4.0" @@ -8459,6 +8599,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "tera" version = "1.20.0" @@ -9634,6 +9785,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs index fe590f6c7d..e8dc3a82b9 100644 --- a/crates/api_models/src/user/dashboard_metadata.rs +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -1,4 +1,5 @@ use common_enums::{CountryAlpha2, MerchantProductType}; +use common_types::primitive_wrappers::SafeString; use common_utils::{id_type, pii}; use masking::Secret; use strum::EnumString; @@ -50,16 +51,16 @@ pub struct ProcessorConnected { #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct OnboardingSurvey { - pub designation: Option, - pub about_business: Option, - pub business_website: Option, - pub hyperswitch_req: Option, - pub major_markets: Option>, - pub business_size: Option, - pub required_features: Option>, - pub required_processors: Option>, - pub planned_live_date: Option, - pub miscellaneous: Option, + pub designation: Option, + pub about_business: Option, + pub business_website: Option, + pub hyperswitch_req: Option, + pub major_markets: Option>, + pub business_size: Option, + pub required_features: Option>, + pub required_processors: Option>, + pub planned_live_date: Option, + pub miscellaneous: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -85,27 +86,27 @@ pub enum ConfigurationType { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct Feedback { pub email: pii::Email, - pub description: Option, + pub description: Option, pub rating: Option, - pub category: Option, + pub category: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct ProdIntent { - pub legal_business_name: Option, - pub business_label: Option, + pub legal_business_name: Option, + pub business_label: Option, pub business_location: Option, - pub display_name: Option, - pub poc_email: Option>, - pub business_type: Option, - pub business_identifier: Option, - pub business_website: Option, - pub poc_name: Option>, - pub poc_contact: Option>, - pub comments: Option, + pub display_name: Option, + pub poc_email: Option, + pub business_type: Option, + pub business_identifier: Option, + pub business_website: Option, + pub poc_name: Option>, + pub poc_contact: Option>, + pub comments: Option, pub is_completed: bool, #[serde(default)] pub product_type: MerchantProductType, - pub business_country_name: Option, + pub business_country_name: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] diff --git a/crates/common_types/src/primitive_wrappers.rs b/crates/common_types/src/primitive_wrappers.rs index 925dcf0af5..ccb6a08bb0 100644 --- a/crates/common_types/src/primitive_wrappers.rs +++ b/crates/common_types/src/primitive_wrappers.rs @@ -1,4 +1,5 @@ pub use bool_wrappers::*; +pub use safe_string::*; pub use u32_wrappers::*; mod bool_wrappers { use std::ops::Deref; @@ -433,3 +434,106 @@ mod u32_wrappers { } } } + +/// Safe string wrapper that validates input against XSS attacks +mod safe_string { + use std::ops::Deref; + + use common_utils::validation::contains_potential_xss_or_sqli; + use masking::SerializableSecret; + use serde::{de::Error, Deserialize, Serialize}; + + /// String wrapper that prevents XSS and SQLi attacks + #[derive(Clone, Debug, Eq, PartialEq, Default)] + pub struct SafeString(String); + + impl SafeString { + /// Creates a new SafeString after XSS and SQL injection validation + pub fn new(value: String) -> Result { + if contains_potential_xss_or_sqli(&value) { + return Err("Input contains potentially malicious content".into()); + } + Ok(Self(value)) + } + + /// Creates a SafeString from a string slice + pub fn from_string_slice(value: &str) -> Result { + Self::new(value.to_string()) + } + + /// Returns the inner string as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consumes self and returns the inner String + pub fn into_inner(self) -> String { + self.0 + } + + /// Returns true if the string is empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the length of the string + pub fn len(&self) -> usize { + self.0.len() + } + } + + impl Deref for SafeString { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + // Custom serialization and deserialization + impl Serialize for SafeString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for SafeString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Self::new(value).map_err(D::Error::custom) + } + } + + // Implement SerializableSecret for SafeString to work with Secret + impl SerializableSecret for SafeString {} + + // Diesel implementations for database operations + impl diesel::serialize::ToSql for SafeString + where + DB: diesel::backend::Backend, + String: diesel::serialize::ToSql, + { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, DB>, + ) -> diesel::serialize::Result { + self.0.to_sql(out) + } + } + + impl diesel::deserialize::FromSql for SafeString + where + DB: diesel::backend::Backend, + String: diesel::deserialize::FromSql, + { + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + String::from_sql(value).map(Self) + } + } +} diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index d61b91c513..ff5f73bd5f 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -60,8 +60,10 @@ thiserror = "1.0.69" time = { version = "0.3.41", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"], optional = true } url = { version = "2.5.4", features = ["serde"] } +urlencoding = "2.1.3" utoipa = { version = "4.2.3", features = ["preserve_order", "preserve_path_order"] } uuid = { version = "1.17.0", features = ["v7"] } +ammonia = "3.3" # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/common_utils/src/validation.rs b/crates/common_utils/src/validation.rs index 2bcad17882..48f0304ca0 100644 --- a/crates/common_utils/src/validation.rs +++ b/crates/common_utils/src/validation.rs @@ -1,5 +1,7 @@ //! Custom validations for some shared types. +#![deny(clippy::invalid_regex)] + use std::{collections::HashSet, sync::LazyLock}; use error_stack::report; @@ -82,6 +84,78 @@ pub fn validate_domain_against_allowed_domains( }) } +/// checks whether the input string contains potential XSS or SQL injection attack vectors +pub fn contains_potential_xss_or_sqli(input: &str) -> bool { + let decoded = urlencoding::decode(input).unwrap_or_else(|_| input.into()); + + // Check for suspicious percent-encoded patterns + static PERCENT_ENCODED: LazyLock> = LazyLock::new(|| { + Regex::new(r"%[0-9A-Fa-f]{2}") + .map_err(|_err| { + #[cfg(feature = "logs")] + logger::error!(?_err); + }) + .ok() + }); + + if decoded.contains('%') { + match PERCENT_ENCODED.as_ref() { + Some(regex) => { + if regex.is_match(&decoded) { + return true; + } + } + None => return true, + } + } + + if ammonia::is_html(&decoded) { + return true; + } + + static XSS: LazyLock> = LazyLock::new(|| { + Regex::new( + r"(?is)\bon[a-z]+\s*=|\bjavascript\s*:|\bdata\s*:\s*text/html|\b(alert|prompt|confirm|eval)\s*\(", + ) + .map_err(|_err| { + #[cfg(feature = "logs")] + logger::error!(?_err); + }) + .ok() + }); + + static SQLI: LazyLock> = LazyLock::new(|| { + Regex::new( + r"(?is)(?:')\s*or\s*'?\d+'?=?\d*|union\s+select|insert\s+into|drop\s+table|information_schema|sleep\s*\(|--|;", + ) + .map_err(|_err| { + #[cfg(feature = "logs")] + logger::error!(?_err); + }) + .ok() + }); + + match XSS.as_ref() { + Some(regex) => { + if regex.is_match(&decoded) { + return true; + } + } + None => return true, + } + + match SQLI.as_ref() { + Some(regex) => { + if regex.is_match(&decoded) { + return true; + } + } + None => return true, + } + + false +} + #[cfg(test)] mod tests { use fake::{faker::internet::en::SafeEmail, Fake}; @@ -150,4 +224,103 @@ mod tests { prop_assert!(validate_email(&email).is_err()); } } + + #[test] + fn detects_basic_script_tags() { + assert!(contains_potential_xss_or_sqli( + "" + )); + } + + #[test] + fn detects_event_handlers() { + assert!(contains_potential_xss_or_sqli( + "onload=alert('xss') onclick=alert('xss') onmouseover=alert('xss')", + )); + } + + #[test] + fn detects_data_url_payload() { + assert!(contains_potential_xss_or_sqli( + "data:text/html,", + )); + } + + #[test] + fn detects_iframe_javascript_src() { + assert!(contains_potential_xss_or_sqli( + "", + )); + } + + #[test] + fn detects_svg_with_script() { + assert!(contains_potential_xss_or_sqli( + "", + )); + } + + #[test] + fn detects_object_with_js() { + assert!(contains_potential_xss_or_sqli( + "", + )); + } + + #[test] + fn detects_mixed_case_tags() { + assert!(contains_potential_xss_or_sqli( + "" + )); + } + + #[test] + fn detects_embedded_script_in_text() { + assert!(contains_potential_xss_or_sqli( + "TestCompany", + )); + } + + #[test] + fn detects_math_with_script() { + assert!(contains_potential_xss_or_sqli( + "", + )); + } + + #[test] + fn detects_basic_sql_tautology() { + assert!(contains_potential_xss_or_sqli("' OR '1'='1")); + } + + #[test] + fn detects_time_based_sqli() { + assert!(contains_potential_xss_or_sqli("' OR SLEEP(5) --")); + } + + #[test] + fn detects_percent_encoded_sqli() { + // %27 OR %271%27=%271 is a typical encoded variant + assert!(contains_potential_xss_or_sqli("%27%20OR%20%271%27%3D%271")); + } + + #[test] + fn detects_benign_html_as_suspicious() { + assert!(contains_potential_xss_or_sqli("Hello")); + } + + #[test] + fn allows_legitimate_plain_text() { + assert!(!contains_potential_xss_or_sqli("My Test Company Ltd.")); + } + + #[test] + fn allows_normal_url() { + assert!(!contains_potential_xss_or_sqli("https://example.com")); + } + + #[test] + fn allows_percent_char_without_encoding() { + assert!(!contains_potential_xss_or_sqli("Get 50% off today")); + } } diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index ece4c60169..d229b9e27f 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -1,9 +1,6 @@ -use std::str::FromStr; - use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; #[cfg(feature = "email")] use common_enums::EntityType; -use common_utils::pii; use diesel_models::{ enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, }; @@ -11,7 +8,7 @@ use error_stack::{report, ResultExt}; use hyperswitch_interfaces::crm::CrmPayload; #[cfg(feature = "email")] use masking::ExposeInterface; -use masking::PeekInterface; +use masking::{PeekInterface, Secret}; use router_env::logger; use crate::{ @@ -456,11 +453,6 @@ async fn insert_metadata( metadata } types::MetaData::ProdIntent(data) => { - if let Some(poc_email) = &data.poc_email { - let inner_poc_email = poc_email.peek().as_str(); - pii::Email::from_str(inner_poc_email) - .change_context(UserErrors::EmailParsingError)?; - } let mut metadata = utils::insert_merchant_scoped_metadata_to_db( state, user.user_id.clone(), @@ -523,19 +515,23 @@ async fn insert_metadata( let hubspot_body = state .crm_client .make_body(CrmPayload { - legal_business_name: data.legal_business_name, - business_label: data.business_label, + legal_business_name: data.legal_business_name.map(|s| s.into_inner()), + business_label: data.business_label.map(|s| s.into_inner()), business_location: data.business_location, - display_name: data.display_name, - poc_email: data.poc_email, - business_type: data.business_type, - business_identifier: data.business_identifier, - business_website: data.business_website, - poc_name: data.poc_name, - poc_contact: data.poc_contact, - comments: data.comments, + display_name: data.display_name.map(|s| s.into_inner()), + poc_email: data.poc_email.map(|s| Secret::new(s.peek().clone())), + business_type: data.business_type.map(|s| s.into_inner()), + business_identifier: data.business_identifier.map(|s| s.into_inner()), + business_website: data.business_website.map(|s| s.into_inner()), + poc_name: data + .poc_name + .map(|s| Secret::new(s.peek().clone().into_inner())), + poc_contact: data + .poc_contact + .map(|s| Secret::new(s.peek().clone().into_inner())), + comments: data.comments.map(|s| s.into_inner()), is_completed: data.is_completed, - business_country_name: data.business_country_name, + business_country_name: data.business_country_name.map(|s| s.into_inner()), }) .await; let base_url = user_utils::get_base_url(state); diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 73c4281843..99a28305f2 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -3,7 +3,7 @@ use common_enums::{EntityType, MerchantProductType}; use common_utils::{errors::CustomResult, pii, types::user::EmailThemeConfig}; use error_stack::ResultExt; use external_services::email::{EmailContents, EmailData, EmailError}; -use masking::{ExposeInterface, Secret}; +use masking::{ExposeInterface, PeekInterface, Secret}; use crate::{configs, consts, routes::SessionState}; #[cfg(feature = "olap")] @@ -567,14 +567,26 @@ impl BizEmailProd { state.conf.email.prod_intent_recipient_email.clone(), )?, settings: state.conf.clone(), - user_name: data.poc_name.unwrap_or_default(), - poc_email: data.poc_email.unwrap_or_default(), - legal_business_name: data.legal_business_name.unwrap_or_default(), + user_name: data + .poc_name + .map(|s| Secret::new(s.peek().clone().into_inner())) + .unwrap_or_default(), + poc_email: data + .poc_email + .map(|s| Secret::new(s.peek().clone())) + .unwrap_or_default(), + legal_business_name: data + .legal_business_name + .map(|s| s.into_inner()) + .unwrap_or_default(), business_location: data .business_location .unwrap_or(common_enums::CountryAlpha2::AD) .to_string(), - business_website: data.business_website.unwrap_or_default(), + business_website: data + .business_website + .map(|s| s.into_inner()) + .unwrap_or_default(), theme_id, theme_config, product_type: data.product_type, diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs index a91a3fe917..0fade67279 100644 --- a/crates/router/src/utils/user/dashboard_metadata.rs +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -287,8 +287,12 @@ pub fn is_prod_email_required(data: &ProdIntent, user_email: String) -> bool { data.poc_email.as_ref().map(|email| email.peek().as_str()), "juspay", ); - let business_website_check = not_contains_string(data.business_website.as_deref(), "juspay") - && not_contains_string(data.business_website.as_deref(), "hyperswitch"); + let business_website_check = + not_contains_string(data.business_website.as_ref().map(|s| s.as_str()), "juspay") + && not_contains_string( + data.business_website.as_ref().map(|s| s.as_str()), + "hyperswitch", + ); let user_email_check = not_contains_string(Some(&user_email), "juspay"); if (poc_email_check && business_website_check && user_email_check).not() {