Noch ein paar Hardening-Sachen

This commit is contained in:
2025-09-25 14:08:45 +00:00
parent 744488fb5b
commit da9d08c149
4 changed files with 80 additions and 67 deletions

View File

@@ -7,10 +7,18 @@ ACME_EMAIL=admin@example.com
# --- Database ---
MYSQL_PASSWORD=change_this_password
MYSQL_ROOT_PASSWORD=change_this_root_password
MYSQL_DATABASE=rezepte
MYSQL_USER=rezepte_user
MYSQL_PORT=3307
# --- Auth / Security ---
JWT_SECRET=please_change_to_secure_32_char_min
CORS_ORIGIN=https://rezepte.${DOMAIN}
ALLOW_INSECURE_CORS=0
BACKEND_PORT=3001
FRONTEND_PORT=3000
MAX_FILE_SIZE=5242880
UPLOAD_PATH=/app/uploads
# --- Images from Registry (override if pushing to GHCR or custom registry) ---
BACKEND_IMAGE=docker.citysensor.de/rezepte-backend:latest
@@ -18,6 +26,8 @@ FRONTEND_IMAGE=docker.citysensor.de/rezepte-frontend:latest
# --- Frontend API URL (if building locally outside compose) ---
PRODUCTION_API_URL=https://rezepte.${DOMAIN}/api
DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
VITE_API_URL=https://rezepte.${DOMAIN}/api
# --- Optional HOST_IP for local network override builds ---
HOST_IP=192.168.1.100

View File

@@ -9,7 +9,6 @@ Dieses Repository enthält zwei Welten: den modernen Node.js / React Stack sowie
| mysql | default | 3307 -> 3306 | MySQL 8 Datenbank |
| backend | default | 3001 -> 3001 | Node.js API (Express + Prisma) |
| frontend | default | 3000 -> 80 | React Build via nginx |
| php-app | legacy | 8082 -> 80 | Altes PHP Frontend |
| phpmyadmin | admin | 8083 -> 80 | DB Verwaltung |
## Node / Modern Stack
@@ -30,23 +29,12 @@ docker compose up -d
```
Öffnen: http://localhost:3000
### Mit Legacy PHP zusätzlich
```bash
docker compose --profile legacy up -d
```
Öffnen: http://localhost:8082
### Mit phpMyAdmin zusätzlich
### Mit phpMyAdmin
```bash
docker compose --profile admin up -d
```
Öffnen: http://localhost:8083
### Beide Zusatz-Profile
```bash
docker compose --profile legacy --profile admin up -d
```
### Alles stoppen
```bash
docker compose down
@@ -79,6 +67,25 @@ Danach Backend neu bauen.
## Datenbankzugang
## Environment (.env)
Beispiel siehe `.env.example`. Wichtigste Variablen:
```
DOMAIN=example.com
MYSQL_DATABASE=rezepte
MYSQL_USER=rezepte_user
MYSQL_PASSWORD=change_this_password
MYSQL_ROOT_PASSWORD=change_this_root_password
BACKEND_PORT=3001
FRONTEND_PORT=3000
DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
JWT_SECRET=please_change_to_secure_32_char_min
CORS_ORIGIN=http://localhost:3000
VITE_API_URL=http://localhost:3001/api
UPLOAD_PATH=/app/uploads
MAX_FILE_SIZE=5242880
```
Nicht gesetzte Variablen fallen auf sichere Defaults (oder Dummy Werte) zurück; vor Produktion immer mit echten Secrets überschreiben.
## Datenbankzugang
### Für die Anwendung:

View File

@@ -30,28 +30,44 @@ const limiter = rateLimit({
});
app.use(limiter);
// CORS configuration - support comma separated origins in CORS_ORIGIN
// NOTE: In docker-compose we temporarily set CORS_ORIGIN="*" for troubleshooting.
// Narrow this down for production: e.g. CORS_ORIGIN="http://esprimo:3000,http://localhost:3000".
// CORS Hardening
// Supports comma separated origins. Wildcard '*' only allowed if ALLOW_INSECURE_CORS=1 and not in production.
const insecureOverride = process.env.ALLOW_INSECURE_CORS === '1';
const isProd = process.env.NODE_ENV === 'production';
let allowedOrigins: string[] = [];
if (config.cors.origin === '*') {
allowedOrigins = ['*'];
} else if (config.cors.origin.includes(',')) {
if (config.cors.origin.includes(',')) {
allowedOrigins = config.cors.origin.split(',').map(o => o.trim()).filter(Boolean);
} else if (config.cors.origin === '*' && (!isProd || insecureOverride)) {
allowedOrigins = ['*'];
} else {
allowedOrigins = [config.cors.origin];
}
// Always add defaults if not already present
// De-dupe & normalize trailing slashes
allowedOrigins = Array.from(new Set(allowedOrigins.map(o => o.replace(/\/$/, ''))));
// Auto-add common localhost dev origins if not prod and not wildcard
if (!isProd && !allowedOrigins.includes('*')) {
['http://localhost:5173','http://localhost:3000'].forEach(def => {
if (!allowedOrigins.includes(def) && !allowedOrigins.includes('*')) allowedOrigins.push(def);
if (!allowedOrigins.includes(def)) allowedOrigins.push(def);
});
}
// If in production and wildcard attempted without override, remove it
if (isProd && allowedOrigins.includes('*') && !insecureOverride) {
console.warn('[CORS] Wildcard removed in production. Set CORS_ORIGIN explicitly or ALLOW_INSECURE_CORS=1 (NOT RECOMMENDED).');
allowedOrigins = allowedOrigins.filter(o => o !== '*');
}
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true); // non-browser (curl, server-side)
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error(`CORS blocked for origin ${origin}`));
if (!origin) return callback(null, true); // Non-browser / same-origin
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin.replace(/\/$/, ''))) {
return callback(null, true);
}
console.warn(`[CORS] Blocked origin: ${origin}`);
return callback(new Error('CORS not allowed for this origin'));
},
credentials: true,
}));
@@ -60,10 +76,11 @@ app.use(cors({
app.use((req, res, next) => {
const origin = req.headers.origin;
const normalized = origin?.replace(/\/$/, '');
if (allowedOrigins.includes('*')) {
res.header('Access-Control-Allow-Origin', origin || '*');
} else if (origin && allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
} else if (normalized && allowedOrigins.includes(normalized)) {
res.header('Access-Control-Allow-Origin', normalized);
}
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');

View File

@@ -7,38 +7,18 @@ services:
profiles:
- default
environment:
MYSQL_ROOT_PASSWORD: rezepte123
MYSQL_DATABASE: rezepte
MYSQL_USER: rezepte_user
MYSQL_PASSWORD: rezepte_pass
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change_this_root_password}
MYSQL_DATABASE: ${MYSQL_DATABASE:-rezepte}
MYSQL_USER: ${MYSQL_USER:-rezepte_user}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-change_this_password}
ports:
- "3307:3306"
- "${MYSQL_PORT:-3307}:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./sql-init:/docker-entrypoint-initdb.d
networks:
- rezepte_network
# PHP Application with Apache
php-app:
build: .
container_name: rezepte_app
restart: always
profiles:
- legacy
ports:
- "8082:80"
volumes:
- .:/var/www/html
depends_on:
- mysql
networks:
- rezepte_network
environment:
DB_HOST: mysql
DB_NAME: rezepte
DB_USER: rezepte_user
DB_PASS: rezepte_pass
# phpMyAdmin
phpmyadmin:
@@ -48,12 +28,12 @@ services:
profiles:
- admin
ports:
- "8083:80"
- "${PHPMYADMIN_PORT:-8083}:80"
environment:
PMA_HOST: mysql
PMA_USER: rezepte_user
PMA_PASSWORD: rezepte_pass
MYSQL_ROOT_PASSWORD: rezepte123
PMA_USER: ${MYSQL_USER:-rezepte_user}
PMA_PASSWORD: ${MYSQL_PASSWORD:-change_this_password}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change_this_root_password}
depends_on:
- mysql
networks:
@@ -68,16 +48,15 @@ services:
profiles:
- default
environment:
NODE_ENV: production
PORT: 3001
DATABASE_URL: mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte
JWT_SECRET: your-super-secret-jwt-key-change-in-production
UPLOAD_PATH: /app/uploads
MAX_FILE_SIZE: 5242880
# CORS_ORIGIN: Restrict in production (example: http://esprimo:3000,http://localhost:3000)
CORS_ORIGIN: "*"
NODE_ENV: ${NODE_ENV:-production}
PORT: ${BACKEND_PORT:-3001}
DATABASE_URL: ${DATABASE_URL:-mysql://${MYSQL_USER:-rezepte_user}:${MYSQL_PASSWORD:-change_this_password}@mysql:3306/${MYSQL_DATABASE:-rezepte}}
JWT_SECRET: ${JWT_SECRET:-please_change_to_secure_32_char_min}
UPLOAD_PATH: ${UPLOAD_PATH:-/app/uploads}
MAX_FILE_SIZE: ${MAX_FILE_SIZE:-5242880}
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:3000}
ports:
- "3001:3001"
- "${BACKEND_PORT:-3001}:${BACKEND_PORT:-3001}"
volumes:
- uploads_data:/app/uploads
- ./uploads:/app/legacy-uploads:ro
@@ -98,7 +77,7 @@ services:
profiles:
- default
ports:
- "3000:80"
- "${FRONTEND_PORT:-3000}:80"
networks:
- rezepte_network
depends_on: