mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(security): add XSS and sqli validation for dashboard metadata fields (#9104)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -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<Option<Regex>> = 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<Option<Regex>> = 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<Option<Regex>> = 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(
|
||||
"<script>alert('xss')</script>"
|
||||
));
|
||||
}
|
||||
|
||||
#[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,<script>alert('xss')</script>",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_iframe_javascript_src() {
|
||||
assert!(contains_potential_xss_or_sqli(
|
||||
"<iframe src=javascript:alert('xss')></iframe>",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_svg_with_script() {
|
||||
assert!(contains_potential_xss_or_sqli(
|
||||
"<svg><script>alert('xss')</script></svg>",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_object_with_js() {
|
||||
assert!(contains_potential_xss_or_sqli(
|
||||
"<object data=javascript:alert('xss')></object>",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_mixed_case_tags() {
|
||||
assert!(contains_potential_xss_or_sqli(
|
||||
"<ScRiPt>alert('xss')</ScRiPt>"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_embedded_script_in_text() {
|
||||
assert!(contains_potential_xss_or_sqli(
|
||||
"Test<script>alert('xss')</script>Company",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_math_with_script() {
|
||||
assert!(contains_potential_xss_or_sqli(
|
||||
"<math><script>alert('xss')</script></math>",
|
||||
));
|
||||
}
|
||||
|
||||
#[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("<b>Hello</b>"));
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user