Security hardening from 2026-07-01 audit

- Exclude supabase/ from Docker build context (leaked service_role key file)
- /api/files: exact per-user namespace match + reject path traversal;
  storage resolveKey rejects ".."/"." segments (fixes cross-user file read)
- Add ownsProperty/Unit/Tenant checks to tenants, maintenance (landlord path),
  and documents (JSON branch, now field-whitelisted) create handlers
- Escape user data in follow-up + payment-link emails (reuse escapeHtml)
- Neutralize CSV formula injection in toCsv + export routes
- Tighter sign-in rate limit (10/min); env-gated email verification + sender
- Per-request nonce CSP; drop script-src 'unsafe-inline' (styles unchanged)
- Add input length bounds; validate follow-ups POST body

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-07-01 13:56:34 -04:00
co-authored by Claude Opus 4.8
parent 857b9a7811
commit 969d5d4c8a
18 changed files with 224 additions and 103 deletions
+40 -3
View File
@@ -21,9 +21,10 @@ export const auth = betterAuth({
}),
emailAndPassword: {
enabled: true,
// Login works immediately; flip to true once verification email is desired.
// Recommended for production: set requireEmailVerification to true.
requireEmailVerification: false,
// Env-gated so production can require a verified email without breaking
// local dev (where RESEND is typically unconfigured). Set
// REQUIRE_EMAIL_VERIFICATION=true in production to enforce.
requireEmailVerification: process.env.REQUIRE_EMAIL_VERIFICATION === "true",
minPasswordLength: 8,
sendResetPassword: async ({ user: u, url }) => {
await sendEmail({
@@ -33,6 +34,18 @@ export const auth = betterAuth({
})
},
},
// Send a verification email on sign-up. Enforcement of verified-email login
// is gated by REQUIRE_EMAIL_VERIFICATION (see emailAndPassword above).
emailVerification: {
sendOnSignUp: true,
sendVerificationEmail: async ({ user: u, url }) => {
await sendEmail({
to: u.email,
subject: "Verify your email — Property Management Network",
html: verifyEmailHtml(url),
})
},
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
@@ -44,6 +57,11 @@ export const auth = betterAuth({
enabled: true,
window: 60, // seconds
max: 20, // requests per window per IP for auth endpoints
customRules: {
// Tighter limit on the password sign-in endpoint to slow credential
// stuffing / brute-force attempts.
"/sign-in/email": { window: 60, max: 10 },
},
},
// Auto-create the app `profiles` row whenever Better Auth creates a user
// (replaces the old `handle_new_user` Postgres trigger).
@@ -88,3 +106,22 @@ function resetPasswordHtml(url: string) {
</body>
</html>`
}
function verifyEmailHtml(url: string) {
return `
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; background: #09090b; color: #fff; padding: 40px 20px; max-width: 560px; margin: 0 auto;">
<div style="background: #16161f; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 32px;">
<h1 style="font-size: 20px; margin: 0 0 8px; color: #fff;">Verify your email</h1>
<p style="color: rgba(255,255,255,0.6); margin: 0 0 24px;">
Confirm your email address to finish setting up your account. If you didn't create an account, you can ignore this email.
</p>
<a href="${url}" style="display: inline-block; background: #6366f1; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">
Verify Email
</a>
<p style="color: rgba(255,255,255,0.4); font-size: 12px; margin: 24px 0 0;">Property Management Network</p>
</div>
</body>
</html>`
}