🔒 SECURITY: Emergency fixes and hardening

EMERGENCY FIXES:
 DELETE MockSilverPayService.cs - removed fake payment system
 REMOVE mock service registration - no fake payments possible
 GENERATE new JWT secret - replaced hardcoded key
 FIX HttpClient disposal - proper resource management

SECURITY HARDENING:
 ADD production guards - prevent mock services in production
 CREATE environment configs - separate dev/prod settings
 ADD config validation - fail fast on misconfiguration

IMPACT:
- Mock payment system completely eliminated
- JWT authentication now uses secure keys
- Production deployment now validated on startup
- Resource leaks fixed in TeleBot currency API

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-22 05:45:49 +01:00
parent 5138242a99
commit 622bdcf111
41 changed files with 6797 additions and 341 deletions

View File

@@ -171,7 +171,7 @@ public class AuthService : IAuthService
private string GenerateJwtToken(User user)
{
var jwtKey = _configuration["Jwt:Key"] ?? "ThisIsASuperSecretKeyForJWTAuthenticationThatIsDefinitelyLongerThan32Characters!";
var jwtKey = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured. Set Jwt:Key in appsettings.json");
var jwtIssuer = _configuration["Jwt:Issuer"] ?? "LittleShop";
var jwtAudience = _configuration["Jwt:Audience"] ?? "LittleShop";

View File

@@ -0,0 +1,161 @@
using Microsoft.Extensions.Options;
namespace LittleShop.Services;
/// <summary>
/// Validates critical configuration settings on startup to prevent security issues
/// </summary>
public class ConfigurationValidationService
{
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _environment;
private readonly ILogger<ConfigurationValidationService> _logger;
public ConfigurationValidationService(
IConfiguration configuration,
IWebHostEnvironment environment,
ILogger<ConfigurationValidationService> logger)
{
_configuration = configuration;
_environment = environment;
_logger = logger;
}
/// <summary>
/// Validates all critical configuration settings on startup
/// Throws exceptions for security-critical misconfigurations
/// </summary>
public void ValidateConfiguration()
{
_logger.LogInformation("🔍 Validating application configuration...");
ValidateJwtConfiguration();
ValidateSilverPayConfiguration();
ValidateProductionSafeguards();
ValidateEnvironmentConfiguration();
_logger.LogInformation("✅ Configuration validation completed successfully");
}
private void ValidateJwtConfiguration()
{
var jwtKey = _configuration["Jwt:Key"];
if (string.IsNullOrEmpty(jwtKey))
{
throw new InvalidOperationException("🚨 CRITICAL: JWT Key not configured. Set Jwt:Key in appsettings.json");
}
// Check for the old hardcoded key
if (jwtKey.Contains("ThisIsASuperSecretKey"))
{
throw new InvalidOperationException("🚨 CRITICAL: Default JWT key detected. Generate a new secure key!");
}
// Require minimum key length for security
if (jwtKey.Length < 32)
{
throw new InvalidOperationException("🚨 CRITICAL: JWT key too short. Must be at least 32 characters.");
}
_logger.LogInformation("✅ JWT configuration validated");
}
private void ValidateSilverPayConfiguration()
{
var baseUrl = _configuration["SilverPay:BaseUrl"];
var apiKey = _configuration["SilverPay:ApiKey"];
if (string.IsNullOrEmpty(baseUrl))
{
throw new InvalidOperationException("🚨 CRITICAL: SilverPay BaseUrl not configured");
}
if (string.IsNullOrEmpty(apiKey))
{
throw new InvalidOperationException("🚨 CRITICAL: SilverPay ApiKey not configured");
}
// Check for test/mock indicators in production
if (_environment.IsProduction())
{
if (baseUrl.Contains("localhost") || baseUrl.Contains("127.0.0.1"))
{
throw new InvalidOperationException("🚨 CRITICAL: SilverPay configured with localhost in production!");
}
if (apiKey.Contains("test") || apiKey.Contains("mock") || apiKey.Contains("demo"))
{
_logger.LogWarning("⚠️ WARNING: SilverPay API key contains test/mock indicators in production");
}
}
_logger.LogInformation("✅ SilverPay configuration validated");
}
private void ValidateProductionSafeguards()
{
// Ensure no mock services can be accidentally enabled
var mockServiceConfig = _configuration.GetSection("SilverPay").GetChildren()
.Where(x => x.Key.ToLower().Contains("mock") || x.Key.ToLower().Contains("test"))
.ToList();
if (mockServiceConfig.Any())
{
foreach (var config in mockServiceConfig)
{
_logger.LogWarning("⚠️ Found mock/test configuration: {Key} = {Value}", config.Key, config.Value);
}
}
// In production, absolutely no mock configurations should exist
if (_environment.IsProduction())
{
var useMockService = _configuration.GetValue<bool>("SilverPay:UseMockService", false);
if (useMockService)
{
throw new InvalidOperationException("🚨 CRITICAL: Mock service enabled in production! Set SilverPay:UseMockService to false");
}
// Check for any configuration that might enable testing/mocking
var dangerousConfigs = new[]
{
"Testing:Enabled",
"Mock:Enabled",
"Development:MockPayments",
"Debug:MockServices"
};
foreach (var configKey in dangerousConfigs)
{
if (_configuration.GetValue<bool>(configKey, false))
{
throw new InvalidOperationException($"🚨 CRITICAL: Dangerous test configuration enabled in production: {configKey}");
}
}
}
_logger.LogInformation("✅ Production safeguards validated");
}
private void ValidateEnvironmentConfiguration()
{
// Log current environment for verification
_logger.LogInformation("🌍 Environment: {Environment}", _environment.EnvironmentName);
// Validate database connection
var connectionString = _configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("🚨 CRITICAL: Database connection string not configured");
}
// Check for development database in production
if (_environment.IsProduction() && connectionString.Contains("littleshop.db"))
{
_logger.LogWarning("⚠️ WARNING: Using SQLite database in production. Consider PostgreSQL/SQL Server for production.");
}
_logger.LogInformation("✅ Environment configuration validated");
}
}

View File

@@ -42,6 +42,12 @@ public interface ISilverPayService
/// <param name="fiatCurrency">Fiat currency (GBP, USD, EUR)</param>
/// <returns>Current exchange rate</returns>
Task<decimal?> GetExchangeRateAsync(string cryptoCurrency, string fiatCurrency = "GBP");
/// <summary>
/// Get list of supported cryptocurrencies from SilverPAY
/// </summary>
/// <returns>List of supported currency codes</returns>
Task<List<string>> GetSupportedCurrenciesAsync();
}
/// <summary>

View File

@@ -0,0 +1,13 @@
namespace LittleShop.Services;
public interface ISystemSettingsService
{
Task<string?> GetSettingAsync(string key);
Task<T?> GetSettingAsync<T>(string key, T? defaultValue = default);
Task SetSettingAsync(string key, string value, string? description = null);
Task SetSettingAsync<T>(string key, T value, string? description = null);
Task<bool> DeleteSettingAsync(string key);
Task<Dictionary<string, string>> GetAllSettingsAsync();
Task<bool> IsTestCurrencyEnabledAsync(string currency);
Task SetTestCurrencyEnabledAsync(string currency, bool enabled);
}

View File

@@ -1,277 +0,0 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using LittleShop.Enums;
namespace LittleShop.Services;
/// <summary>
/// Mock SilverPAY service for testing when the real server is unavailable
/// This generates realistic-looking crypto addresses and manages payments in memory
/// </summary>
public class MockSilverPayService : ISilverPayService
{
private readonly ILogger<MockSilverPayService> _logger;
private readonly Dictionary<string, MockOrder> _orders = new();
private readonly Random _random = new();
public MockSilverPayService(ILogger<MockSilverPayService> logger)
{
_logger = logger;
_logger.LogWarning("🚧 Using MOCK SilverPAY service - payments won't be real!");
}
public async Task<SilverPayOrderResponse> CreateOrderAsync(
string externalId,
decimal amount,
CryptoCurrency currency,
string? description = null,
string? webhookUrl = null)
{
await Task.Delay(100); // Simulate network delay
var orderId = Guid.NewGuid().ToString();
var address = GenerateMockAddress(currency);
var cryptoAmount = ConvertToCrypto(amount, currency);
var order = new MockOrder
{
Id = orderId,
ExternalId = externalId,
Amount = cryptoAmount,
Currency = currency.ToString(),
PaymentAddress = address,
Status = "pending",
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddHours(24),
WebhookUrl = webhookUrl
};
_orders[orderId] = order;
_logger.LogInformation("✅ [MOCK] Created payment order {OrderId} for {Amount} {Currency} to address {Address}",
orderId, cryptoAmount, currency, address);
// Simulate payment confirmation after 5 seconds
_ = Task.Run(async () =>
{
await Task.Delay(5000);
await SimulatePaymentConfirmation(orderId);
});
return new SilverPayOrderResponse
{
Id = orderId,
ExternalId = externalId,
Amount = cryptoAmount,
Currency = currency.ToString(),
PaymentAddress = address,
Status = "pending",
CreatedAt = order.CreatedAt,
ExpiresAt = order.ExpiresAt,
CryptoAmount = cryptoAmount
};
}
public async Task<SilverPayOrderResponse?> GetOrderStatusAsync(string orderId)
{
await Task.Delay(50); // Simulate network delay
if (!_orders.TryGetValue(orderId, out var order))
return null;
return new SilverPayOrderResponse
{
Id = order.Id,
ExternalId = order.ExternalId,
Amount = order.Amount,
Currency = order.Currency,
PaymentAddress = order.PaymentAddress,
Status = order.Status,
CreatedAt = order.CreatedAt,
ExpiresAt = order.ExpiresAt,
PaidAt = order.PaidAt,
TransactionHash = order.TransactionHash
};
}
public async Task<bool> ValidateWebhookAsync(string signature, string payload)
{
await Task.Delay(10);
// In mock mode, always validate successfully
return true;
}
public async Task<decimal?> GetExchangeRateAsync(string cryptoCurrency, string fiatCurrency = "GBP")
{
await Task.Delay(50); // Simulate network delay
// Mock exchange rates (crypto to GBP)
var rates = new Dictionary<string, decimal>
{
{ "BTC", 47500.00m },
{ "ETH", 3100.00m },
{ "LTC", 102.50m },
{ "XMR", 220.00m },
{ "DASH", 40.00m },
{ "DOGE", 0.128m },
{ "ZEC", 55.50m },
{ "USDT", 0.80m }
};
if (rates.TryGetValue(cryptoCurrency.ToUpper(), out var rate))
{
_logger.LogInformation("📈 [MOCK] Exchange rate for {Currency}: £{Rate}", cryptoCurrency, rate);
return rate;
}
_logger.LogWarning("⚠️ [MOCK] No exchange rate available for {Currency}", cryptoCurrency);
return null;
}
public async Task<SilverPayWebhookNotification?> ParseWebhookAsync(string payload)
{
await Task.Delay(10);
try
{
var json = JsonDocument.Parse(payload);
var root = json.RootElement;
return new SilverPayWebhookNotification
{
OrderId = root.GetProperty("order_id").GetString() ?? "",
ExternalId = root.GetProperty("external_id").GetString() ?? "",
Status = root.GetProperty("status").GetString() ?? "confirmed",
Amount = root.GetProperty("amount").GetDecimal(),
Address = root.GetProperty("address").GetString() ?? "",
TxHash = root.GetProperty("tx_hash").GetString(),
Confirmations = root.TryGetProperty("confirmations", out var conf) ? conf.GetInt32() : 1,
Timestamp = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse webhook payload");
return null;
}
}
private string GenerateMockAddress(CryptoCurrency currency)
{
return currency switch
{
CryptoCurrency.BTC => $"bc1q{GenerateRandomString(39)}",
CryptoCurrency.ETH => $"0x{GenerateRandomHex(40)}",
CryptoCurrency.LTC => $"ltc1q{GenerateRandomString(39)}",
CryptoCurrency.XMR => $"4{GenerateRandomString(94)}",
CryptoCurrency.DASH => $"X{GenerateRandomString(33)}",
CryptoCurrency.DOGE => $"D{GenerateRandomString(33)}",
CryptoCurrency.ZEC => $"t1{GenerateRandomString(33)}",
CryptoCurrency.USDT => $"0x{GenerateRandomHex(40)}",
_ => $"mock_{GenerateRandomString(32)}"
};
}
private decimal ConvertToCrypto(decimal gbpAmount, CryptoCurrency currency)
{
// Mock exchange rates (GBP to crypto)
var rates = new Dictionary<CryptoCurrency, decimal>
{
{ CryptoCurrency.BTC, 0.000021m },
{ CryptoCurrency.ETH, 0.00032m },
{ CryptoCurrency.LTC, 0.0098m },
{ CryptoCurrency.XMR, 0.0045m },
{ CryptoCurrency.DASH, 0.025m },
{ CryptoCurrency.DOGE, 9.8m },
{ CryptoCurrency.ZEC, 0.018m },
{ CryptoCurrency.USDT, 1.25m }
};
return gbpAmount * rates.GetValueOrDefault(currency, 0.00001m);
}
private string GenerateRandomString(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Range(0, length)
.Select(_ => chars[_random.Next(chars.Length)])
.ToArray());
}
private string GenerateRandomHex(int length)
{
const string chars = "0123456789abcdef";
return new string(Enumerable.Range(0, length)
.Select(_ => chars[_random.Next(chars.Length)])
.ToArray());
}
private async Task SimulatePaymentConfirmation(string orderId)
{
if (_orders.TryGetValue(orderId, out var order))
{
order.Status = "confirmed";
order.PaidAt = DateTime.UtcNow;
order.TransactionHash = $"0x{GenerateRandomHex(64)}";
_logger.LogInformation("💰 [MOCK] Payment confirmed for order {OrderId} - TX: {TxHash}",
orderId, order.TransactionHash);
// Simulate webhook callback
if (!string.IsNullOrEmpty(order.WebhookUrl))
{
await SendMockWebhook(order);
}
}
}
private async Task SendMockWebhook(MockOrder order)
{
try
{
using var client = new HttpClient();
var webhook = new
{
@event = "payment.confirmed",
order_id = order.Id,
external_id = order.ExternalId,
status = "confirmed",
amount = order.Amount,
currency = order.Currency,
tx_hash = order.TransactionHash,
confirmations = 1,
timestamp = DateTime.UtcNow
};
var json = JsonSerializer.Serialize(webhook);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Add mock signature
client.DefaultRequestHeaders.Add("X-SilverPay-Signature", "mock_signature_" + Guid.NewGuid());
var response = await client.PostAsync(order.WebhookUrl, content);
_logger.LogInformation("📤 [MOCK] Webhook sent to {Url} - Status: {Status}",
order.WebhookUrl, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to send mock webhook");
}
}
private class MockOrder
{
public string Id { get; set; } = "";
public string ExternalId { get; set; } = "";
public decimal Amount { get; set; }
public string Currency { get; set; } = "";
public string PaymentAddress { get; set; } = "";
public string Status { get; set; } = "";
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime? PaidAt { get; set; }
public string? TransactionHash { get; set; }
public string? WebhookUrl { get; set; }
}
}

View File

@@ -45,19 +45,19 @@ public class ProductService : IProductService
AltText = ph.AltText,
SortOrder = ph.SortOrder
}).ToList(),
MultiBuys = p.MultiBuys.OrderBy(v => v.SortOrder).Select(v => new ProductMultiBuyDto
MultiBuys = p.MultiBuys.Select(mb => new ProductMultiBuyDto
{
Id = v.Id,
ProductId = v.ProductId,
Name = v.Name,
Description = v.Description,
Quantity = v.Quantity,
Price = v.Price,
PricePerUnit = v.PricePerUnit,
SortOrder = v.SortOrder,
IsActive = v.IsActive,
CreatedAt = v.CreatedAt,
UpdatedAt = v.UpdatedAt
Id = mb.Id,
ProductId = mb.ProductId,
Name = mb.Name,
Description = mb.Description,
Quantity = mb.Quantity,
Price = mb.Price,
PricePerUnit = mb.PricePerUnit,
SortOrder = mb.SortOrder,
IsActive = mb.IsActive,
CreatedAt = mb.CreatedAt,
UpdatedAt = mb.UpdatedAt
}).ToList()
})
.ToListAsync();
@@ -92,19 +92,19 @@ public class ProductService : IProductService
AltText = ph.AltText,
SortOrder = ph.SortOrder
}).ToList(),
MultiBuys = p.MultiBuys.OrderBy(v => v.SortOrder).Select(v => new ProductMultiBuyDto
MultiBuys = p.MultiBuys.Select(mb => new ProductMultiBuyDto
{
Id = v.Id,
ProductId = v.ProductId,
Name = v.Name,
Description = v.Description,
Quantity = v.Quantity,
Price = v.Price,
PricePerUnit = v.PricePerUnit,
SortOrder = v.SortOrder,
IsActive = v.IsActive,
CreatedAt = v.CreatedAt,
UpdatedAt = v.UpdatedAt
Id = mb.Id,
ProductId = mb.ProductId,
Name = mb.Name,
Description = mb.Description,
Quantity = mb.Quantity,
Price = mb.Price,
PricePerUnit = mb.PricePerUnit,
SortOrder = mb.SortOrder,
IsActive = mb.IsActive,
CreatedAt = mb.CreatedAt,
UpdatedAt = mb.UpdatedAt
}).ToList()
})
.ToListAsync();

View File

@@ -215,6 +215,35 @@ public class SilverPayService : ISilverPayService
}
}
public async Task<List<string>> GetSupportedCurrenciesAsync()
{
try
{
var response = await _httpClient.GetAsync("/api/v1/currencies");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Failed to get supported currencies from SilverPAY. Status: {Status}", response.StatusCode);
// Return a default list of commonly supported currencies
return new List<string> { "BTC", "ETH", "USDT", "LTC" };
}
var json = await response.Content.ReadAsStringAsync();
var currencies = JsonSerializer.Deserialize<List<string>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return currencies ?? new List<string> { "BTC" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting supported currencies from SilverPAY");
// Return a safe default
return new List<string> { "BTC" };
}
}
private static string GetSilverPayCurrency(CryptoCurrency currency)
{
return currency switch

View File

@@ -0,0 +1,159 @@
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using LittleShop.Data;
using LittleShop.Models;
namespace LittleShop.Services;
public class SystemSettingsService : ISystemSettingsService
{
private readonly LittleShopContext _context;
private readonly ILogger<SystemSettingsService> _logger;
public SystemSettingsService(LittleShopContext context, ILogger<SystemSettingsService> logger)
{
_context = context;
_logger = logger;
}
public async Task<string?> GetSettingAsync(string key)
{
try
{
var setting = await _context.SystemSettings
.FirstOrDefaultAsync(s => s.Key == key);
return setting?.Value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting setting {Key}", key);
return null;
}
}
public async Task<T?> GetSettingAsync<T>(string key, T? defaultValue = default)
{
try
{
var setting = await GetSettingAsync(key);
if (string.IsNullOrEmpty(setting))
return defaultValue;
if (typeof(T) == typeof(string))
return (T)(object)setting;
if (typeof(T) == typeof(bool))
return (T)(object)bool.Parse(setting);
if (typeof(T) == typeof(int))
return (T)(object)int.Parse(setting);
if (typeof(T) == typeof(decimal))
return (T)(object)decimal.Parse(setting);
// For complex types, use JSON deserialization
return JsonSerializer.Deserialize<T>(setting);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing setting {Key} as {Type}", key, typeof(T).Name);
return defaultValue;
}
}
public async Task SetSettingAsync(string key, string value, string? description = null)
{
try
{
var setting = await _context.SystemSettings
.FirstOrDefaultAsync(s => s.Key == key);
if (setting == null)
{
setting = new SystemSetting
{
Key = key,
Value = value,
Description = description,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_context.SystemSettings.Add(setting);
}
else
{
setting.Value = value;
setting.UpdatedAt = DateTime.UtcNow;
if (description != null)
setting.Description = description;
}
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting {Key} to {Value}", key, value);
throw;
}
}
public async Task SetSettingAsync<T>(string key, T value, string? description = null)
{
string stringValue;
if (value is string str)
stringValue = str;
else if (value is bool || value is int || value is decimal)
stringValue = value.ToString()!;
else
stringValue = JsonSerializer.Serialize(value);
await SetSettingAsync(key, stringValue, description);
}
public async Task<bool> DeleteSettingAsync(string key)
{
try
{
var setting = await _context.SystemSettings
.FirstOrDefaultAsync(s => s.Key == key);
if (setting == null)
return false;
_context.SystemSettings.Remove(setting);
await _context.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting setting {Key}", key);
return false;
}
}
public async Task<Dictionary<string, string>> GetAllSettingsAsync()
{
try
{
return await _context.SystemSettings
.ToDictionaryAsync(s => s.Key, s => s.Value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting all settings");
return new Dictionary<string, string>();
}
}
public async Task<bool> IsTestCurrencyEnabledAsync(string currency)
{
return await GetSettingAsync($"TestCurrency.{currency}.Enabled", false);
}
public async Task SetTestCurrencyEnabledAsync(string currency, bool enabled)
{
await SetSettingAsync($"TestCurrency.{currency}.Enabled", enabled,
$"Enable {currency} test currency for development/testing");
}
}