feat(security): enhance input validation and security measures across various routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s
This commit is contained in:
@@ -168,7 +168,7 @@ router.post('/register', registerLimiter, async (req, res) => {
|
||||
return res.status(409).json({ error: 'Username is already taken' });
|
||||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 12);
|
||||
const hash = await bcrypt.hash(password, 12);
|
||||
|
||||
// If SMTP is configured, require email verification
|
||||
if (isMailerConfigured()) {
|
||||
@@ -352,7 +352,7 @@ router.post('/login', loginLimiter, async (req, res) => {
|
||||
}
|
||||
|
||||
const token = generateToken(user.id);
|
||||
const { password_hash, ...safeUser } = user;
|
||||
const { password_hash, verification_token, verification_token_expires, verification_resend_at, ...safeUser } = user;
|
||||
|
||||
res.json({ token, user: safeUser });
|
||||
} catch (err) {
|
||||
@@ -485,7 +485,7 @@ router.put('/password', authenticateToken, passwordLimiter, async (req, res) =>
|
||||
return res.status(400).json({ error: `New password must be at least ${MIN_PASSWORD_LENGTH} characters long` });
|
||||
}
|
||||
|
||||
const hash = bcrypt.hashSync(newPassword, 12);
|
||||
const hash = await bcrypt.hash(newPassword, 12);
|
||||
await db.run('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hash, req.user.id]);
|
||||
|
||||
res.json({ message: 'Password changed successfully' });
|
||||
@@ -498,7 +498,7 @@ router.put('/password', authenticateToken, passwordLimiter, async (req, res) =>
|
||||
// POST /api/auth/avatar - Upload avatar image
|
||||
router.post('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
|
||||
try {
|
||||
// Validate content type
|
||||
// Validate file content by checking magic bytes (file signatures)
|
||||
const contentType = req.headers['content-type'];
|
||||
if (!contentType || !contentType.startsWith('image/')) {
|
||||
return res.status(400).json({ error: 'Only image files are allowed' });
|
||||
@@ -528,7 +528,18 @@ router.post('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Image must not exceed 2MB' });
|
||||
}
|
||||
|
||||
const ext = contentType.includes('png') ? 'png' : contentType.includes('gif') ? 'gif' : contentType.includes('webp') ? 'webp' : 'jpg';
|
||||
// Validate magic bytes to prevent Content-Type spoofing
|
||||
const magicBytes = buffer.slice(0, 8);
|
||||
const isJPEG = magicBytes[0] === 0xFF && magicBytes[1] === 0xD8 && magicBytes[2] === 0xFF;
|
||||
const isPNG = magicBytes[0] === 0x89 && magicBytes[1] === 0x50 && magicBytes[2] === 0x4E && magicBytes[3] === 0x47;
|
||||
const isGIF = magicBytes[0] === 0x47 && magicBytes[1] === 0x49 && magicBytes[2] === 0x46;
|
||||
const isWEBP = magicBytes[0] === 0x52 && magicBytes[1] === 0x49 && magicBytes[2] === 0x46 && magicBytes[3] === 0x46
|
||||
&& buffer.length > 11 && buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50;
|
||||
if (!isJPEG && !isPNG && !isGIF && !isWEBP) {
|
||||
return res.status(400).json({ error: 'File content does not match a supported image format (JPEG, PNG, GIF, WebP)' });
|
||||
}
|
||||
|
||||
const ext = isPNG ? 'png' : isGIF ? 'gif' : isWEBP ? 'webp' : 'jpg';
|
||||
const filename = `${req.user.id}_${Date.now()}.${ext}`;
|
||||
const filepath = path.join(uploadsDir, filename);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user