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:
@@ -78,16 +78,37 @@ public class FileWatcherService : BackgroundService
|
||||
var importService = scope.ServiceProvider.GetRequiredService<IImportService>();
|
||||
|
||||
// Find the target account by matching filename to account name
|
||||
// In multi-user setups, the filename format is "username--accountname.ext"
|
||||
// If no separator, match against all accounts (single-user mode)
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(e.Name);
|
||||
var account = await db.Accounts
|
||||
.Where(a => a.IsActive && !a.IsClosed)
|
||||
.FirstOrDefaultAsync(a => a.Name.ToLower() == fileNameWithoutExt!.ToLower());
|
||||
string? targetUsername = null;
|
||||
string accountMatch = fileNameWithoutExt!;
|
||||
|
||||
// Fall back to first active account
|
||||
account ??= await db.Accounts
|
||||
.Where(a => a.IsActive && !a.IsClosed)
|
||||
.OrderBy(a => a.SortOrder)
|
||||
.FirstOrDefaultAsync();
|
||||
if (fileNameWithoutExt!.Contains("--"))
|
||||
{
|
||||
var parts = fileNameWithoutExt.Split("--", 2);
|
||||
targetUsername = parts[0];
|
||||
accountMatch = parts[1];
|
||||
}
|
||||
|
||||
var accountQuery = db.Accounts
|
||||
.Include(a => a.User)
|
||||
.Where(a => a.IsActive && !a.IsClosed);
|
||||
|
||||
if (targetUsername != null)
|
||||
accountQuery = accountQuery.Where(a => a.User.Username.ToLower() == targetUsername.ToLower());
|
||||
|
||||
var account = await accountQuery
|
||||
.FirstOrDefaultAsync(a => a.Name.ToLower() == accountMatch.ToLower());
|
||||
|
||||
// Fall back to first active account only if no username filter (single-user mode)
|
||||
if (account == null && targetUsername == null)
|
||||
{
|
||||
account = await db.Accounts
|
||||
.Where(a => a.IsActive && !a.IsClosed)
|
||||
.OrderBy(a => a.SortOrder)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user