Security Best Practices
Essential security guidelines for developing secure Crosser modules.
Credential Management
Never Store Sensitive Data Directly
❌ Never expose sensitive settings directly:
public class UnsafeSettings : FlowModuleSettings
{
public string Password { get; set; } // DON'T DO THIS
public string ApiKey { get; set; } // DON'T DO THIS
public string ConnectionString { get; set; } // DON'T DO THIS
}
✅ Always use credential references:
public class SecureSettings : FlowModuleSettings
{
[JsonSchemaExtensionData("x-credential", "UsernamePassword")]
[Display(Name = "Service Credentials")]
public Guid? ServiceCredential { get; set; }
[JsonSchemaExtensionData("x-credential", "ApiKey")]
[Display(Name = "API Key")]
public Guid? ApiCredential { get; set; }
[JsonSchemaExtensionData("x-credential", "ConnectionString")]
[Display(Name = "Database Connection")]
public Guid? DatabaseCredential { get; set; }
}
Secure Credential Access
public override async Task<IError?> Initialize()
{
if (!this.Settings.ApiCredential.HasValue)
{
return new Error("API credentials not configured");
}
// Retrieve credentials securely
var credentials = await this.GetCredentialContentAsync<CredentialWithApiKey>(
this.Settings.ApiCredential.Value);
if (credentials.IsError || credentials.Value is null)
{
return credentials.Error ?? new Error("Failed to get API credential");
}
// Use credentials safely
this.httpClient.DefaultRequestHeaders.Add("Authorization",
$"Bearer {credentials.Value.ApiKey}");
// ❌ NEVER log credentials
// Information("Using API key: {ApiKey}", credentials.Value.ApiKey);
// ✅ Log safely without secrets
Information("API authenticated successfully");
return await base.Initialize();
}
Input Validation and Sanitization
Validate All External Input
Always validate settings in the Validate method:
public override void Validate(SettingsValidator validator)
{
// 1. Validate required fields
if (string.IsNullOrWhiteSpace(this.ApiEndpoint))
{
validator.AddError(nameof(this.ApiEndpoint), "API endpoint is required");
return;
}
// 2. Validate URL format and enforce HTTPS
if (!Uri.TryCreate(this.ApiEndpoint, UriKind.Absolute, out var uri) ||
!uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
{
validator.AddError(nameof(this.ApiEndpoint),
"API endpoint must be a valid HTTPS URL");
}
// 3. Validate port ranges
if (this.Port < 1 || this.Port > 65535)
{
validator.AddError(nameof(this.Port),
$"Port must be between 1 and 65535, got {this.Port}");
}
// 4. Validate input patterns
if (!string.IsNullOrWhiteSpace(this.Identifier) &&
!Regex.IsMatch(this.Identifier, @"^[a-zA-Z0-9_-]+$"))
{
validator.AddError(nameof(this.Identifier),
"Identifier can only contain letters, numbers, underscores, and hyphens");
}
}
Sanitize Message Content
Validate and sanitize incoming message data:
protected override async Task MessageReceived(IFlowMessage message)
{
try
{
var userInput = message.Get<string>("userInput");
// Validate input exists
if (string.IsNullOrWhiteSpace(userInput))
{
var error = "Input cannot be empty";
this.SetStatus(Status.Warning, error);
message.SetError(error);
return;
}
// Sanitize HTML/XML characters
var sanitized = userInput.Trim()
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
// Validate length
if (sanitized.Length > 1000)
{
var error = "Input exceeds maximum length of 1000 characters";
this.SetStatus(Status.Warning, error);
message.SetError(error);
return;
}
message.Set("sanitizedInput", sanitized);
}
catch (Exception ex)
{
Error(ex, "Input validation failed");
var error = "Input validation failed";
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
finally
{
await this.Next(message);
}
}
Prevent SQL Injection
When working with databases, always use parameterized queries:
// ❌ WRONG - Vulnerable to SQL injection
var query = $"SELECT * FROM users WHERE name = '{userName}'";
// ✅ CORRECT - Use parameterized queries
var query = "SELECT * FROM users WHERE name = @name";
using var command = this.connection.CreateCommand();
command.CommandText = query;
var param = command.CreateParameter();
param.ParameterName = "@name";
param.Value = userName;
command.Parameters.Add(param);
Secure Logging
Never Log Sensitive Information
protected override async Task MessageReceived(IFlowMessage message)
{
try
{
var credentials = await this.GetCredentialContentAsync<CredentialWithUsernamePassword>(
this.Settings.ServiceCredential.Value);
// ✅ Safe logging
Information("Processing message for user {Username}",
credentials?.Value?.Username);
// ❌ NEVER log passwords, tokens, or keys
// Information("Password: {Password}", credentials.Value.Password);
// Information("Full message: {Content}", message.ToJSON()); // May contain sensitive data
var result = await this.ProcessSecurely(message, credentials.Value);
// ✅ Log results without sensitive data
Information("Processing completed successfully for user {Username}",
credentials?.Value?.Username);
}
catch (Exception ex)
{
// ✅ Log errors without exposing sensitive details
Error(ex, "Processing failed");
var error = "Processing failed";
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
finally
{
await this.Next(message);
}
}
Safe Logging Patterns
// ✅ SAFE - Log operation without data
Information("Database query executed successfully");
// ✅ SAFE - Log counts and statistics
Information("Processed {Count} records in {ElapsedMs}ms", count, elapsed);
// ✅ SAFE - Log non-sensitive identifiers
Information("Processing order {OrderId} for customer {CustomerId}", orderId, customerId);
// ❌ UNSAFE - Don't log entire messages
// Debug("Received message: {@Message}", message);
// ❌ UNSAFE - Don't log credentials
// Debug("Using connection: {ConnectionString}", connectionString);
// ❌ UNSAFE - Don't log API keys
// Debug("API Key: {ApiKey}", apiKey);
Error Handling Security
Avoid Information Disclosure
Never expose internal system details in error messages:
protected override async Task MessageReceived(IFlowMessage message)
{
try
{
await this.ProcessMessage(message);
}
catch (SqlException ex)
{
// ❌ Don't expose internal details
// message.SetError($"SQL Error: {ex.Message}");
// message.SetError($"Query failed: {query}");
// ✅ Use generic error messages
var error = "Database operation failed";
Error(ex, "Database operation failed");
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
catch (UnauthorizedAccessException ex)
{
// ✅ Handle security exceptions appropriately
Warning(ex, "Access denied for operation");
var error = "Access denied";
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
catch (HttpRequestException ex)
{
// ❌ Don't expose URLs or endpoints
// message.SetError($"Failed to connect to {url}: {ex.Message}");
// ✅ Generic external service error
Error(ex, "External service request failed");
var error = "External service unavailable";
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
catch (Exception ex)
{
// ✅ Generic handling for unexpected errors
Error(ex, "Unexpected error occurred");
var error = "An error occurred while processing the message";
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
finally
{
await this.Next(message);
}
}
Secure Communication
Enforce HTTPS
Always use HTTPS for external communications:
public override void Validate(SettingsValidator validator)
{
if (!string.IsNullOrWhiteSpace(this.ApiEndpoint))
{
if (Uri.TryCreate(this.ApiEndpoint, UriKind.Absolute, out var uri))
{
if (!uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
{
validator.AddError(nameof(this.ApiEndpoint),
"Only HTTPS endpoints are allowed for security");
}
}
}
}
Certificate Validation
// ❌ NEVER disable certificate validation in production
// ServicePointManager.ServerCertificateValidationCallback =
// (sender, cert, chain, sslPolicyErrors) => true;
// ✅ Use proper certificate handling
var handler = new HttpClientHandler();
if (this.Settings.AllowSelfSignedCerts)
{
// Only allow in non-production environments
if (this.Settings.Environment.Equals("Development",
StringComparison.OrdinalIgnoreCase))
{
handler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
}
Configuration Security
Use Secure Defaults
public class SecureModuleSettings : FlowModuleSettings
{
[Display(Name = "Enable SSL")]
[DefaultValue(true)]
public bool EnableSsl { get; set; } = true; // Secure by default
[Range(1, 300)]
[Display(Name = "Timeout (seconds)")]
[DefaultValue(30)]
public int TimeoutSeconds { get; set; } = 30;
[Display(Name = "Allow Self-Signed Certificates")]
[DefaultValue(false)]
public bool AllowSelfSignedCerts { get; set; } = false; // Secure default
[Display(Name = "Environment")]
[DefaultValue("Production")]
public string Environment { get; set; } = "Production";
public override void Validate(SettingsValidator validator)
{
// Warn about insecure configurations
if (!this.EnableSsl &&
this.Environment.Equals("Production", StringComparison.OrdinalIgnoreCase))
{
validator.AddError(nameof(this.EnableSsl),
"SSL is disabled - connection will not be encrypted");
}
if (this.AllowSelfSignedCerts &&
this.Environment.Equals("Production", StringComparison.OrdinalIgnoreCase))
{
validator.AddError(nameof(this.AllowSelfSignedCerts),
"Self-signed certificates should not be allowed in production");
}
base.Validate(validator);
}
}
Resource Management Security
Secure File Handling
When working with resources:
public override async Task<IError?> Initialize()
{
if (!this.Settings.ConfigFile.HasValue)
{
return new Error("Configuration file not specified");
}
// Get resource securely
var resource = await this.GetResourceContentAsync<string>(this.Settings.ConfigFile.Value);
if (!resource.IsSuccess)
{
return new Error("Failed to load configuration file");
}
var fileContent = resource.Result;
// Validate file content before parsing
if (string.IsNullOrEmpty(fileContent))
{
return new Error("Configuration file is empty");
}
// Limit file size to prevent DoS
if (fileContent.Length > 1000000) // 1MB limit
{
return new Error("Configuration file exceeds maximum size of 1MB");
}
try
{
// Parse with error handling
this.configuration = JsonSerializer.Deserialize<ModuleConfiguration>(fileContent);
if (this.configuration == null)
{
return new Error("Invalid configuration format");
}
}
catch (JsonException ex)
{
Error(ex, "Failed to parse configuration file");
return new Error("Configuration file format is invalid");
}
return await base.Initialize();
}
Security Checklist
Use this checklist when developing modules:
Credentials
- [ ] No passwords or keys in settings properties
- [ ] All sensitive data uses credential references
- [ ] Credentials never logged
- [ ] Credential access includes null checks
Input Validation
- [ ] All settings validated in
Validatemethod - [ ] Message properties validated before use
- [ ] String lengths checked
- [ ] Input sanitized for injection attacks
- [ ] HTTPS enforced for URLs
Error Handling
- [ ] Generic error messages to users
- [ ] Detailed errors only in logs
- [ ] No internal implementation details exposed
- [ ] Exception types handled appropriately
Logging
- [ ] No credentials in logs
- [ ] No sensitive user data in logs
- [ ] No full message content in logs
- [ ] Error messages don't expose internals
Communication
- [ ] HTTPS used for external APIs
- [ ] Certificate validation enabled
- [ ] Timeouts configured
- [ ] Secure defaults used
Best Practices
- [ ] Secure by default configuration
- [ ] Validation warnings for insecure settings
- [ ] Production environment checks
- [ ] Resource size limits enforced
Common Security Mistakes
1. Logging Credentials
// ❌ NEVER DO THIS
Information("Connecting with password: {Password}", password);
Information("API Key: {Key}", apiKey);
Debug("Full config: {@Settings}", Settings);
2. Exposing Internal Errors
// ❌ NEVER DO THIS
catch (Exception ex)
{
message.SetError(ex.ToString()); // Exposes stack traces
message.SetError($"Query: {sqlQuery}"); // Exposes SQL
}
3. Weak Input Validation
// ❌ NEVER DO THIS
var id = message.Get<string>("id");
var query = $"SELECT * FROM users WHERE id = {id}"; // SQL injection!
4. Disabling Security Features
// ❌ NEVER DO THIS
ServicePointManager.ServerCertificateValidationCallback =
(sender, cert, chain, sslPolicyErrors) => true; // Accepts any certificate!
Summary
Security is not optional. Follow these principles:
- Never expose credentials - Use credential references
- Validate all input - Trust nothing from external sources
- Log safely - Never log sensitive information
- Use generic error messages - Don't expose internals
- Enforce HTTPS - Secure communication by default
- Secure defaults - Make the safe choice the default choice
Remember: A security vulnerability in your module can compromise the entire flow and potentially the entire system. Always code defensively and assume all input is malicious until proven otherwise.