fix(security): rate limiting, CORS restriction, tableName allowlist, non-root Docker, migration entrypoint, aria-live

This commit is contained in:
Experiments DB Dev
2026-04-15 13:25:18 -04:00
parent 5460a93217
commit 9b3c883bf0
11 changed files with 146 additions and 15 deletions
+24 -2
View File
@@ -2,6 +2,7 @@ const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const { globalErrorHandler } = require('./middleware/errorHandler');
const experimentsRouter = require('./routes/experiments');
@@ -11,19 +12,40 @@ const auditLogsRouter = require('./routes/auditLogs');
const app = express();
app.use(helmet());
// Security headers
app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' },
}));
// CORS — restrict to the configured frontend origin in production
const allowedOrigin = process.env.CORS_ORIGIN || '*';
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
origin: allowedOrigin,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Rate limiting — 100 requests per minute per IP on all API routes
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' },
skip: () => process.env.NODE_ENV === 'test',
});
app.use(express.json({ limit: '1mb' }));
app.use(morgan(process.env.NODE_ENV === 'test' ? 'silent' : process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
// Health check (no rate limit)
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Apply rate limiter to all API routes
app.use('/api', apiLimiter);
app.use('/api/experiments', experimentsRouter);
app.use('/api/animals', animalsRouter);
app.use('/api/daily-statuses', dailyStatusesRouter);
+1 -6
View File
@@ -7,12 +7,7 @@ async function startServer() {
try {
await prisma.$connect();
console.log('Database connected successfully');
if (process.env.NODE_ENV === 'production') {
const { execSync } = require('child_process');
execSync('npx prisma migrate deploy', { stdio: 'inherit' });
}
// Note: migrations are run by docker-entrypoint.sh before this process starts
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
+1 -1
View File
@@ -7,7 +7,7 @@ const { validateRequest } = require('../middleware/errorHandler');
router.get(
'/',
[
query('tableName').optional().isString(),
query('tableName').optional().isIn(['experiments', 'animals', 'daily_statuses', 'audit_logs']).withMessage('tableName must be one of: experiments, animals, daily_statuses, audit_logs'),
query('recordId').optional().matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).withMessage('recordId must be a valid UUID'),
query('limit').optional().isInt({ min: 1, max: 200 }).withMessage('limit must be 1200').toInt(),
query('offset').optional().isInt({ min: 0 }).withMessage('offset must be ≥0').toInt(),