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
+3 -3
View File
@@ -237,7 +237,7 @@ async function loadConversations() {
conversations.value = data
conversationsLoaded.value = true
} catch (err) {
console.error('Failed to load conversations', err)
console.error('Failed to load conversations:', err instanceof Error ? err.message : 'Unknown error')
}
}
@@ -248,7 +248,7 @@ async function loadConversation(id: string) {
messages.value = data.messages
await scrollToBottom()
} catch (err) {
console.error('Failed to load conversation', err)
console.error('Failed to load conversation:', err instanceof Error ? err.message : 'Unknown error')
}
}
@@ -322,7 +322,7 @@ async function deleteConversation(id: string) {
startNewChat()
}
} catch (err) {
console.error('Failed to delete conversation', err)
console.error('Failed to delete conversation:', err instanceof Error ? err.message : 'Unknown error')
}
}
+3 -3
View File
@@ -162,7 +162,7 @@ async function loadConversations() {
const { data } = await chatApi.getConversations()
conversations.value = data
} catch (err) {
console.error('Failed to load conversations', err)
console.error('Failed to load conversations:', err instanceof Error ? err.message : 'Unknown error')
}
}
@@ -173,7 +173,7 @@ async function loadConversation(id: string) {
messages.value = data.messages
await scrollToBottom()
} catch (err) {
console.error('Failed to load conversation', err)
console.error('Failed to load conversation:', err instanceof Error ? err.message : 'Unknown error')
}
}
@@ -244,7 +244,7 @@ async function deleteConversation(id: string) {
startNewChat()
}
} catch (err) {
console.error('Failed to delete conversation', err)
console.error('Failed to delete conversation:', err instanceof Error ? err.message : 'Unknown error')
}
}
+1 -1
View File
@@ -553,7 +553,7 @@ async function fetchReport<T>(request: Promise<{ data: T }>): Promise<T | null>
} catch (err: any) {
const status = err?.response?.status
if (status && status >= 500) {
console.error('Report request failed:', err)
console.error('Report request failed:', err instanceof Error ? err.message : 'Unknown error')
return null
}
// 4xx (including 404) treated as no data