feat(payout_link): secure payout links using server side validations and client side headers (#5219)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-07-17 18:23:55 +05:30
committed by GitHub
parent 35c9b8afe1
commit 2d204c9f73
23 changed files with 803 additions and 357 deletions

View File

@ -99,5 +99,21 @@ pub const MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 64;
/// Minimum allowed length for MerchantReferenceId
pub const MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 1;
/// Regex for matching a domain
/// Eg -
/// http://www.example.com
/// https://www.example.com
/// www.example.com
/// example.io
pub const STRICT_DOMAIN_REGEX: &str = r"^(https?://)?(([A-Za-z0-9][-A-Za-z0-9]\.)*[A-Za-z0-9][-A-Za-z0-9]*|(\d{1,3}\.){3}\d{1,3})+(:[0-9]{2,4})?$";
/// Regex for matching a wildcard domain
/// Eg -
/// *.example.com
/// *.subdomain.domain.com
/// *://example.com
/// *example.com
pub const WILDCARD_DOMAIN_REGEX: &str = r"^((\*|https?)?://)?((\*\.|[A-Za-z0-9][-A-Za-z0-9]*\.)*[A-Za-z0-9][-A-Za-z0-9]*|((\d{1,3}|\*)\.){3}(\d{1,3}|\*)|\*)(:\*|:[0-9]{2,4})?(/\*)?$";
/// Maximum allowed length for MerchantName
pub const MAX_ALLOWED_MERCHANT_NAME_LENGTH: usize = 64;

View File

@ -1,4 +1,4 @@
//! Common
//! This module has common utilities for links in HyperSwitch
use std::{collections::HashSet, primitive::i64};
@ -13,10 +13,13 @@ use diesel::{
};
use error_stack::{report, ResultExt};
use masking::Secret;
use regex::Regex;
#[cfg(feature = "logs")]
use router_env::logger;
use serde::Serialize;
use utoipa::ToSchema;
use crate::{errors::ParsingError, id_type, types::MinorUnit};
use crate::{consts, errors::ParsingError, id_type, types::MinorUnit};
#[derive(
Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, FromSqlRow, AsExpression, ToSchema,
@ -162,6 +165,8 @@ pub struct PayoutLinkData {
pub amount: MinorUnit,
/// Payout currency
pub currency: enums::Currency,
/// A list of allowed domains (glob patterns) where this link can be embedded / opened from
pub allowed_domains: HashSet<String>,
}
crate::impl_to_sql_from_sql_json!(PayoutLinkData);
@ -209,3 +214,140 @@ pub struct EnabledPaymentMethod {
#[schema(value_type = HashSet<PaymentMethodType>)]
pub payment_method_types: HashSet<enums::PaymentMethodType>,
}
/// Util function for validating a domain without any wildcard characters.
pub fn validate_strict_domain(domain: &str) -> bool {
Regex::new(consts::STRICT_DOMAIN_REGEX)
.map(|regex| regex.is_match(domain))
.map_err(|err| {
let err_msg = format!("Invalid strict domain regex: {err:?}");
#[cfg(feature = "logs")]
logger::error!(err_msg);
err_msg
})
.unwrap_or(false)
}
/// Util function for validating a domain with "*" wildcard characters.
pub fn validate_wildcard_domain(domain: &str) -> bool {
Regex::new(consts::WILDCARD_DOMAIN_REGEX)
.map(|regex| regex.is_match(domain))
.map_err(|err| {
let err_msg = format!("Invalid strict domain regex: {err:?}");
#[cfg(feature = "logs")]
logger::error!(err_msg);
err_msg
})
.unwrap_or(false)
}
#[cfg(test)]
mod domain_tests {
use regex::Regex;
use super::*;
#[test]
fn test_validate_strict_domain_regex() {
assert!(
Regex::new(consts::STRICT_DOMAIN_REGEX).is_ok(),
"Strict domain regex is invalid"
);
}
#[test]
fn test_validate_wildcard_domain_regex() {
assert!(
Regex::new(consts::WILDCARD_DOMAIN_REGEX).is_ok(),
"Wildcard domain regex is invalid"
);
}
#[test]
fn test_validate_strict_domain() {
let valid_domains = vec![
"example.com",
"example.subdomain.com",
"https://example.com:8080",
"http://example.com",
"example.com:8080",
"example.com:443",
"localhost:443",
"127.0.0.1:443",
];
for domain in valid_domains {
assert!(
validate_strict_domain(domain),
"Could not validate strict domain: {}",
domain
);
}
let invalid_domains = vec![
"",
"invalid.domain.",
"not_a_domain",
"http://example.com/path?query=1#fragment",
"127.0.0.1.2:443",
];
for domain in invalid_domains {
assert!(
!validate_strict_domain(domain),
"Could not validate invalid strict domain: {}",
domain
);
}
}
#[test]
fn test_validate_wildcard_domain() {
let valid_domains = vec![
"example.com",
"example.subdomain.com",
"https://example.com:8080",
"http://example.com",
"example.com:8080",
"example.com:443",
"localhost:443",
"127.0.0.1:443",
"*.com",
"example.*.com",
"example.com:*",
"*:443",
"localhost:*",
"127.0.0.*:*",
"*:*",
];
for domain in valid_domains {
assert!(
validate_wildcard_domain(domain),
"Could not validate wildcard domain: {}",
domain
);
}
let invalid_domains = vec![
"",
"invalid.domain.",
"not_a_domain",
"http://example.com/path?query=1#fragment",
"*.",
".*",
"example.com:*:",
"*:443:",
":localhost:*",
"127.00.*:*",
];
for domain in invalid_domains {
assert!(
!validate_wildcard_domain(domain),
"Could not validate invalid wildcard domain: {}",
domain
);
}
}
}