Private
Public Access
1
0

Security hardening: fix IDOR vulnerabilities, add rate limiting, SSRF protection, and encryption upgrades

- Fix ReconciliationController and BankSync LinkAccount/UpdateLinkedAccount IDOR (verify account ownership)
- Add ASP.NET Core rate limiting: strict on auth endpoints, global on all API routes
- Harden SSRF validation on Ollama URL (IPv6-mapped bypass, link-local, 0.0.0.0 blocking)
- Upgrade encryption from AES-CBC to AES-GCM with HKDF key derivation (backward-compatible decrypt)
- Add startup validation that rejects default JWT/encryption keys in Production
- Add security headers (X-Frame-Options, CSP, HSTS, Cache-Control, nosniff) to API and nginx
- Mask account numbers in API responses (show last 4 digits only)
- Add password strength validation (min 8 chars) and BCrypt work factor 12
- Hash refresh tokens (SHA-256) before database storage
- Set JWT ClockSkew to zero for immediate expiry enforcement
- Add file upload size limit (10 MB) and filename sanitization
- Cap transaction search PageSize to 200
- Fix FileWatcherService cross-user account matching in multi-user deployments
- Sanitize console.error calls to prevent token leakage in browser logs
- Parameterize raw SQL in SimpleFinConnectionMigration
- Make AuthService.GenerateAuthResponse fully async

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-16 12:53:20 -05:00
parent fd14473125
commit 2f389ab8e9
19 changed files with 345 additions and 50 deletions
+50
View File
@@ -112,6 +112,7 @@ public class ChatService : IChatService
// Load AI settings
var settings = await _db.AiCategorizationSettings.FirstOrDefaultAsync(s => s.UserId == userId);
var ollamaUrl = settings?.OllamaUrl ?? "http://localhost:11434";
ValidateOllamaUrl(ollamaUrl);
var modelName = settings?.ModelName ?? "llama3.1:8b";
var chatContextSize = settings?.ChatContextSize ?? 16384;
var chatBotName = settings?.ChatBotName;
@@ -1281,4 +1282,53 @@ public class ChatService : IChatService
}
return int.TryParse(value?.ToString(), out var result) ? result : null;
}
private static void ValidateOllamaUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
throw new InvalidOperationException("Invalid Ollama URL");
if (uri.Scheme != "http" && uri.Scheme != "https")
throw new InvalidOperationException("Ollama URL must use http or https");
var host = uri.Host.ToLowerInvariant();
if (host == "localhost" || host == "host.docker.internal")
return;
if (host == "metadata.google.internal")
throw new InvalidOperationException("Ollama URL points to a blocked address");
if (System.Net.IPAddress.TryParse(host, out var ip))
{
if (ip.IsIPv4MappedToIPv6)
ip = ip.MapToIPv4();
if (!IsAllowedIp(ip))
throw new InvalidOperationException("Ollama URL must point to a local or private network address");
return;
}
if (!host.Contains('.'))
return;
throw new InvalidOperationException("Ollama URL must point to a local or private network address");
}
private static bool IsAllowedIp(System.Net.IPAddress ip)
{
if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
return System.Net.IPAddress.IsLoopback(ip);
var bytes = ip.GetAddressBytes();
if (bytes[0] == 0) return false;
if (bytes[0] == 169 && bytes[1] == 254) return false;
if (bytes[0] == 127) return true;
if (bytes[0] == 10) return true;
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true;
if (bytes[0] == 192 && bytes[1] == 168) return true;
return false;
}
}