Table of Contents

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("<", "&lt;")
            .Replace(">", "&gt;")
            .Replace("\"", "&quot;")
            .Replace("'", "&#39;");

        // 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 Validate method
  • [ ] 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:

  1. Never expose credentials - Use credential references
  2. Validate all input - Trust nothing from external sources
  3. Log safely - Never log sensitive information
  4. Use generic error messages - Don't expose internals
  5. Enforce HTTPS - Secure communication by default
  6. 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.

>> Performance Optimization