security: trust proxy fix, login rate limiting, default secret banners (#33)
* fix(security): default-secret banners, login rate limiting, real-IP passthrough
- app.set('trust proxy', 1) so req.ip resolves to real client IP behind nginx
- loginRateLimit() middleware on POST /api/auth/login; limits read from
app_settings (login_rate_limit_max / login_rate_limit_window_sec) with
60-second cache; falls back to 10/60s when Redis/DB unavailable
- GET /api/auth/security-status returns warnings for default JWT_SECRET,
ENCRYPTION_KEY, and admin/admin credential — non-breaking for dev
- SecurityBanners component in AppLayout: yellow banner for default secrets,
red banner + change-password link for default admin password
- Login rate limit settings exposed in Settings > General tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): rate-limit /security-status, suppress CodeQL false positive on /login
- Add allMethods option to rateLimitRedis so it can apply to GET routes
- Apply rateLimitRedis (10 req/60s per user) to GET /auth/security-status
to prevent bcrypt-based CPU exhaustion by authenticated users
- Add lgtm suppression comment on POST /login — CodeQL does not recognize
our custom loginRateLimit() middleware; rate limiting is in place
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Denis Evers <git@evers.sh>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Denis Evers
parent
0350a76232
commit
a336b0e207
@@ -366,6 +366,38 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Rate Limit */}
|
||||
<div className="card p-5">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">Login Rate Limiting</h3>
|
||||
<p className="text-xs text-gray-400 dark:text-slate-500 mb-4">
|
||||
Limits login attempts per IP address to protect against brute-force attacks.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ key: 'login_rate_limit_max', label: 'Max attempts', unit: 'attempts' },
|
||||
{ key: 'login_rate_limit_window_sec', label: 'Time window', unit: 'sec' },
|
||||
].map(({ key, label, unit }) => (
|
||||
<div key={key} className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-slate-300">{label}</div>
|
||||
<div className="text-xs text-gray-400">{unit}</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
className="input w-24 text-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
value={settings[key] as number ?? ''}
|
||||
onChange={(e) =>
|
||||
updateSettingsMutation.mutate({ [key]: parseInt(e.target.value) })
|
||||
}
|
||||
min="1"
|
||||
step="1"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAC Scan */}
|
||||
<div className="card p-5">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">MAC Scan</h3>
|
||||
|
||||
Reference in New Issue
Block a user