Update README and configuration to replace RSA with Ed25519 for federation security
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m30s

This commit is contained in:
2026-02-28 20:19:59 +01:00
parent 2831f80ab4
commit c281628fdc
8 changed files with 74 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ import redis from '../config/redis.js';
import { authenticateToken, generateToken } from '../middleware/auth.js';
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
import { themes } from '../../src/themes/index.js';
import { languages } from '../../src/i18n/index.js';
if (!process.env.JWT_SECRET) {
console.error('FATAL: JWT_SECRET environment variable is not set.');
@@ -35,7 +36,7 @@ function makeRedisStore(prefix) {
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
const VALID_THEMES = new Set(themes.map(t => t.id));
const VALID_LANGUAGES = new Set(['en', 'de']);
const VALID_LANGUAGES = new Set(Object.keys(languages));
// Allowlist for CSS color values only permits hsl(), hex (#rgb/#rrggbb) and plain names
const SAFE_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|[a-zA-Z]{1,30})$/;
@@ -88,6 +89,16 @@ const avatarLimiter = rateLimit({
store: makeRedisStore('rl:avatar:'),
});
// S1: rate limit resend-verification to prevent SMTP abuse
const resendVerificationLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests. Please try again later.' },
store: makeRedisStore('rl:resend:'),
});
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const uploadsDir = path.join(__dirname, '..', '..', 'uploads', 'avatars');
@@ -224,7 +235,7 @@ router.get('/verify-email', async (req, res) => {
});
// POST /api/auth/resend-verification
router.post('/resend-verification', async (req, res) => {
router.post('/resend-verification', resendVerificationLimiter, async (req, res) => {
try {
const { email } = req.body;
if (!email) {
@@ -494,8 +505,9 @@ router.post('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
const db = getDb();
const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]);
if (current?.avatar_image) {
const oldPath = path.join(uploadsDir, current.avatar_image);
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
// S8: defense-in-depth path traversal check on DB-stored filename
const oldPath = path.resolve(uploadsDir, current.avatar_image);
if (oldPath.startsWith(uploadsDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
}
fs.writeFileSync(filepath, buffer);
@@ -515,8 +527,9 @@ router.delete('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
const db = getDb();
const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]);
if (current?.avatar_image) {
const oldPath = path.join(uploadsDir, current.avatar_image);
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
// S8: defense-in-depth path traversal check on DB-stored filename
const oldPath = path.resolve(uploadsDir, current.avatar_image);
if (oldPath.startsWith(uploadsDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
}
await db.run('UPDATE users SET avatar_image = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]);
const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);