Priority: High
Estimated Time: 3-4 hours
Labels: security, validation, middleware, input-sanitization
Dependencies: Issue #8 (Error Handling)
Overview
Implement comprehensive request validation middleware that sanitizes inputs, validates data types, checks formats, prevents injection attacks, and ensures all incoming data meets expected schemas before reaching business logic.
What You're Building
A request validation system that:
- Validates JSON request bodies
- Sanitizes string inputs (prevents XSS, SQL injection)
- Validates wallet addresses (Stellar format)
- Validates amounts (positive decimals, within limits)
- Validates phone numbers (Nigerian format)
- Validates email addresses
- Checks required fields
- Enforces data type constraints
- Returns clear validation error messages
This prevents invalid/malicious data from entering the system.
Key Requirements
1. Input Sanitization
Sanitize All String Inputs:
Remove Dangerous Characters:
- HTML/JavaScript tags (prevent XSS)
- SQL special characters (prevent injection)
- Control characters
- Excessive whitespace
- Unicode exploits
Sanitization Rules:
Input: "<script>alert('xss')</script>Hello"
Output: "Hello"
Input: "'; DROP TABLE users; --"
Output: "DROP TABLE users"
Input: "Normal text with spaces"
Output: "Normal text with spaces"
What to Sanitize:
- Account names
- Email addresses
- Narration fields
- Memo fields
- Any user-provided text
2. Wallet Address Validation
Stellar Wallet Format:
Validation Rules:
- Exactly 56 characters
- Starts with 'G'
- Contains only valid base32 characters (A-Z, 2-7)
- Valid checksum
Validation:
fn validate_stellar_address(address: &str) -> Result<()> {
// Length check
if address.len() != 56 {
return Err("Wallet address must be 56 characters");
}
// Starts with G
if !address.starts_with('G') {
return Err("Stellar address must start with 'G'");
}
// Valid base32
let valid_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
if !address.chars().all(|c| valid_chars.contains(c)) {
return Err("Invalid characters in wallet address");
}
// Optional: Verify checksum with stellar-sdk
PublicKey::from_account_id(address)?;
Ok(())
}
3. Amount Validation
Monetary Values:
Validation Rules:
- Must be positive (> 0)
- Maximum 2 decimal places for NGN/cNGN
- Within acceptable range (min/max)
- Not NaN or Infinity
- Valid decimal format
Validation:
fn validate_amount(
amount: &str,
min: Decimal,
max: Decimal
) -> Result<Decimal> {
// Parse as decimal
let decimal = Decimal::from_str(amount)
.map_err(|_| "Invalid amount format")?;
// Check positive
if decimal <= Decimal::ZERO {
return Err("Amount must be greater than zero");
}
// Check range
if decimal < min {
return Err(format!("Amount below minimum: {}", min));
}
if decimal > max {
return Err(format!("Amount above maximum: {}", max));
}
// Check decimal places (2 for currency)
if decimal.scale() > 2 {
return Err("Amount can have maximum 2 decimal places");
}
Ok(decimal)
}
4. Phone Number Validation
Nigerian Phone Numbers:
Format Rules:
- 11 digits starting with 0
- Or 13 digits starting with +234
- Valid network prefixes (080, 081, 070, 090, etc.)
Validation:
fn validate_nigerian_phone(phone: &str) -> Result<String> {
// Remove spaces and dashes
let cleaned = phone.replace([' ', '-'], "");
// Pattern: 0XXXXXXXXXXX (11 digits)
if cleaned.starts_with('0') && cleaned.len() == 11 {
// Validate network prefix
let prefix = &cleaned[0..4];
let valid_prefixes = [
"0803", "0806", "0810", "0813", "0814", "0816", // MTN
"0802", "0808", "0812", "0901", "0902", "0904", // Airtel
"0805", "0807", "0811", "0815", "0905", "0915", // Glo
"0809", "0817", "0818", "0908", "0909" // 9mobile
];
if valid_prefixes.contains(&prefix) {
return Ok(cleaned);
}
}
// Pattern: +234XXXXXXXXXX (13 digits)
if cleaned.starts_with("+234") && cleaned.len() == 14 {
// Convert to local format
let local = format!("0{}", &cleaned[4..]);
return validate_nigerian_phone(&local);
}
Err("Invalid Nigerian phone number format")
}
5. Email Validation
Email Format:
Validation Rules:
- Valid email format (RFC 5322)
- Contains @ symbol
- Has domain
- Reasonable length (< 254 chars)
Validation:
fn validate_email(email: &str) -> Result<String> {
// Basic format check
let email_regex = Regex::new(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
)?;
if !email_regex.is_match(email) {
return Err("Invalid email format");
}
// Length check
if email.len() > 254 {
return Err("Email too long");
}
// Normalize (lowercase)
Ok(email.to_lowercase())
}
6. Schema Validation
Define Request Schemas:
Example Schemas:
#[derive(Deserialize, Validate)]
struct OnrampQuoteRequest {
#[validate(custom = "validate_stellar_address")]
wallet_address: String,
#[validate(custom = "validate_currency")]
from_currency: String,
#[validate(custom = "validate_currency")]
to_currency: String,
#[validate(range(min = 100, max = 5000000))]
amount: String,
}
#[derive(Deserialize, Validate)]
struct BillPaymentRequest {
#[validate(custom = "validate_stellar_address")]
wallet_address: String,
#[validate(length(min = 1))]
provider_id: String,
#[serde(flatten)]
payment_details: HashMap<String, Value>,
}
Validation Rules by Endpoint
Onramp Quote
- ✓ wallet_address: Stellar format
- ✓ from_currency: "NGN"
- ✓ to_currency: "cNGN"
- ✓ amount: 100 - 5,000,000
Offramp Quote
- ✓ wallet_address: Stellar format
- ✓ from_currency: "cNGN"
- ✓ to_currency: "NGN"
- ✓ amount: 100 - 5,000,000
Bill Payment
- ✓ wallet_address: Stellar format
- ✓ provider_id: Non-empty, exists in providers list
- ✓ meter_number: 10-13 digits (if electricity)
- ✓ phone_number: Nigerian format (if airtime)
- ✓ amount: Positive, within provider limits
Authentication
- ✓ wallet_address: Stellar format
- ✓ message: Non-empty
- ✓ signature: Base64 encoded
- ✓ nonce: UUID format
Error Response Format
Validation Error (400 Bad Request):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"errors": [
{
"field": "wallet_address",
"message": "Invalid Stellar wallet address format",
"value": "GXXX",
"expected": "56 character Stellar address starting with 'G'"
},
{
"field": "amount",
"message": "Amount must be greater than minimum",
"value": "50",
"minimum": "100"
}
]
}
}
Single Field Error:
{
"error": {
"code": "INVALID_WALLET_ADDRESS",
"message": "Wallet address must be 56 characters",
"field": "wallet_address",
"provided": "GXXX"
}
}
Acceptance Criteria
Testing Checklist
Implementation Steps
- Create validation module in
src/middleware/validation.rs
- Implement wallet address validator
- Implement amount validator
- Implement phone number validator
- Implement email validator
- Implement string sanitizer
- Create validation middleware
- Add schema validation with serde
- Implement error formatter
- Write comprehensive tests
Validation Middleware
Apply to Routes:
// Validate request body
app.route(
"/api/onramp/quote",
post(create_quote)
.layer(middleware::from_fn(validate_request::<OnrampQuoteRequest>))
);
// Custom validation
app.route(
"/api/bills/pay",
post(pay_bill)
.layer(middleware::from_fn(validate_bill_payment))
);
Middleware Implementation:
async fn validate_request<T>(
State(state): State<AppState>,
req: Request,
next: Next,
) -> Result<Response, ValidationError>
where
T: DeserializeOwned + Validate
{
// Parse body
let body: T = extract_json(&req).await?;
// Validate
body.validate()
.map_err(|e| ValidationError::from(e))?;
// Continue
Ok(next.run(req).await)
}
Common Validation Patterns
Required Field:
#[validate(length(min = 1))]
field: String
Numeric Range:
#[validate(range(min = 100, max = 5000000))]
amount: u64
Custom Validator:
#[validate(custom = "validate_stellar_address")]
wallet_address: String
Email:
#[validate(email)]
email: String
Length:
#[validate(length(min = 10, max = 13))]
meter_number: String
Security Considerations
SQL Injection Prevention:
- Use parameterized queries (SQLx already does this)
- Sanitize any dynamic SQL (avoid if possible)
- Never concatenate user input into SQL
XSS Prevention:
- Sanitize HTML tags from inputs
- Encode output when rendering
- Use Content-Security-Policy headers
Path Traversal:
- Validate file paths
- Prevent ../ sequences
- Whitelist allowed paths
Command Injection:
- Never pass user input to shell commands
- Use library functions instead of system calls
Unicode Exploits:
- Normalize unicode strings
- Reject unusual unicode characters
- Validate string encoding
Sanitization Functions
Remove HTML Tags:
fn sanitize_html(input: &str) -> String {
// Remove < and > to prevent tags
input.replace(['<', '>'], "")
}
Remove SQL Characters:
fn sanitize_sql(input: &str) -> String {
// Remove potentially dangerous SQL chars
input.replace(['\'', '"', ';', '-', '\\'], "")
}
Normalize Whitespace:
fn normalize_whitespace(input: &str) -> String {
input.split_whitespace().collect::<Vec<_>>().join(" ")
}
Complete Sanitization:
fn sanitize_string(input: &str) -> String {
let mut cleaned = input.to_string();
cleaned = sanitize_html(&cleaned);
cleaned = normalize_whitespace(&cleaned);
cleaned.trim().to_string()
}
Provider-Specific Validation
Electricity (EKEDC):
fn validate_meter_number(meter: &str) -> Result<()> {
if meter.len() < 10 || meter.len() > 13 {
return Err("Meter number must be 10-13 digits");
}
if !meter.chars().all(|c| c.is_numeric()) {
return Err("Meter number must contain only digits");
}
Ok(())
}
Cable TV (DSTV):
fn validate_smart_card(card: &str) -> Result<()> {
if card.len() != 10 {
return Err("Smart card number must be 10 digits");
}
if !card.chars().all(|c| c.is_numeric()) {
return Err("Smart card must contain only digits");
}
Ok(())
}
Validation Error Collection
Collect Multiple Errors:
struct ValidationErrors {
errors: Vec<FieldError>
}
impl ValidationErrors {
fn new() -> Self {
Self { errors: Vec::new() }
}
fn add(&mut self, field: &str, message: &str) {
self.errors.push(FieldError {
field: field.to_string(),
message: message.to_string()
});
}
fn is_empty(&self) -> bool {
self.errors.is_empty()
}
fn into_response(self) -> Response {
// Convert to error response
}
}
Monitoring & Logging
Track Validation Failures:
- Log validation errors (not values)
- Track which fields fail most
- Identify potential attack patterns
- Monitor validation error rate
Alerts:
- Sudden spike in validation errors
- Repeated SQL injection attempts
- Repeated XSS attempts
- Unusual input patterns
Notes
- Validate early (before expensive operations)
- Return all validation errors at once (better UX)
- Be specific in error messages
- Don't leak sensitive info in errors
- Sanitize both input and output
- Use established validation libraries
- Test with malicious inputs
- Keep validation rules updated
- Document validation requirements
- Consider validation as part of API contract
Resources
- OWASP Input Validation Cheat Sheet
- validator Rust crate documentation
- Regex for email/phone validation
- SQL injection prevention techniques
- XSS prevention best practices
Success =
✅ All inputs validated before processing
✅ SQL injection prevented
✅ XSS attacks blocked
✅ Clear validation error messages
✅ Robust security layer!
Priority: High
Estimated Time: 3-4 hours
Labels:
security,validation,middleware,input-sanitizationDependencies: Issue #8 (Error Handling)
Overview
Implement comprehensive request validation middleware that sanitizes inputs, validates data types, checks formats, prevents injection attacks, and ensures all incoming data meets expected schemas before reaching business logic.
What You're Building
A request validation system that:
This prevents invalid/malicious data from entering the system.
Key Requirements
1. Input Sanitization
Sanitize All String Inputs:
Remove Dangerous Characters:
Sanitization Rules:
What to Sanitize:
2. Wallet Address Validation
Stellar Wallet Format:
Validation Rules:
Validation:
3. Amount Validation
Monetary Values:
Validation Rules:
Validation:
4. Phone Number Validation
Nigerian Phone Numbers:
Format Rules:
Validation:
5. Email Validation
Email Format:
Validation Rules:
Validation:
6. Schema Validation
Define Request Schemas:
Example Schemas:
Validation Rules by Endpoint
Onramp Quote
Offramp Quote
Bill Payment
Authentication
Error Response Format
Validation Error (400 Bad Request):
{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "errors": [ { "field": "wallet_address", "message": "Invalid Stellar wallet address format", "value": "GXXX", "expected": "56 character Stellar address starting with 'G'" }, { "field": "amount", "message": "Amount must be greater than minimum", "value": "50", "minimum": "100" } ] } }Single Field Error:
{ "error": { "code": "INVALID_WALLET_ADDRESS", "message": "Wallet address must be 56 characters", "field": "wallet_address", "provided": "GXXX" } }Acceptance Criteria
Testing Checklist
Implementation Steps
src/middleware/validation.rsValidation Middleware
Apply to Routes:
Middleware Implementation:
Common Validation Patterns
Required Field:
Numeric Range:
Custom Validator:
Email:
Length:
Security Considerations
SQL Injection Prevention:
XSS Prevention:
Path Traversal:
Command Injection:
Unicode Exploits:
Sanitization Functions
Remove HTML Tags:
Remove SQL Characters:
Normalize Whitespace:
Complete Sanitization:
Provider-Specific Validation
Electricity (EKEDC):
Cable TV (DSTV):
Validation Error Collection
Collect Multiple Errors:
Monitoring & Logging
Track Validation Failures:
Alerts:
Notes
Resources
Success =
✅ All inputs validated before processing
✅ SQL injection prevented
✅ XSS attacks blocked
✅ Clear validation error messages
✅ Robust security layer!