security: comprehensive hardening across frontend, backend, and infrastructure

- Disable production source maps and hide nginx version
- Reduce JSON body limit from 10gb to 50mb (uploads use multer, not JSON)
- Strip database info and error details from health endpoint
- Mask reCAPTCHA secret key in admin settings API responses
- Whitelist sort/order query parameters in events and photos endpoints
- Stop reflecting arbitrary origins in static file CORS headers
- Align nginx security headers with backend Helmet CSP, remove deprecated X-XSS-Protection
- Strip EXIF metadata from generated thumbnails and hero images
- Bind postgres/redis dev ports to localhost in docker-compose configs
- Add safeExec utility (spawn with shell:false) to prevent command injection
- Convert all exec/execAsync calls in backup, restore, and database backup
  services to use safe spawn-based helpers
This commit is contained in:
Paul Nothaft
2026-02-16 22:33:20 +01:00
parent 50c09904a9
commit 2b25d81144
12 changed files with 214 additions and 92 deletions
+3 -2
View File
@@ -495,8 +495,9 @@ router.get('/', adminAuth, requirePermission('events.view'), async (req, res) =>
const offset = (page - 1) * limit;
const search = req.query.search || '';
const status = req.query.status || 'all';
const sortBy = req.query.sortBy || 'created_at';
const sortOrder = req.query.sortOrder || 'desc';
const allowedSortBy = ['created_at', 'event_name', 'slug', 'updated_at', 'expires_at', 'capture_date'];
const sortBy = allowedSortBy.includes(req.query.sortBy) ? req.query.sortBy : 'created_at';
const sortOrder = ['asc', 'desc'].includes(req.query.sortOrder) ? req.query.sortOrder : 'desc';
// Build query
let query = db('events');
+2 -1
View File
@@ -727,7 +727,8 @@ router.get('/:eventId/photos/:photoId/download', adminAuth, requirePermission('p
router.get('/:eventId/photos', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const { eventId } = req.params;
const { category_id, type, search, sort = 'date', order = 'desc' } = req.query;
const { category_id, type, search, sort = 'date' } = req.query;
const order = ['asc', 'desc'].includes(req.query.order) ? req.query.order : 'desc';
let query = db('photos')
.where({ 'photos.event_id': eventId })
+10
View File
@@ -122,6 +122,11 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res)
}
});
// Mask sensitive secrets before sending to client
if (settingsObject.security_recaptcha_secret_key) {
settingsObject.security_recaptcha_secret_key = '••••••••';
}
res.json(settingsObject);
} catch (error) {
console.error('Settings fetch error:', error);
@@ -160,6 +165,11 @@ router.get('/:type', adminAuth, requirePermission('settings.view'), async (req,
}
});
// Mask sensitive secrets before sending to client
if (settingsObject.security_recaptcha_secret_key) {
settingsObject.security_recaptcha_secret_key = '••••••••';
}
res.json(settingsObject);
} catch (error) {
console.error('Settings fetch error:', error);