- Pin DNS resolution to prevent SSRF rebinding between check and connect (GameDataDownloader) - Fail-closed on realpath failure in GameZipServer - Zip Slip validation before extraction (RuffleService) - Escape LIKE wildcards in ActivityService queries - Sanitize sort order to prevent SQL injection - Whitelist sort order instead of passing raw user input - Add headersSent guard to global error handler - Remove cookie from CGI allowed headers, sanitize content-type against null/CR/LF injection - Revoke refresh tokens on password change and user deactivation - Wrap role/user uniqueness checks in transactions (TOCTOU fix) - Fix concurrent ZIP mount race with deferred promise and double-close prevention via _manuallyClosed flag - Handle client disconnect in collectRequestBody - Tighten nginx CSP, repeat headers in location blocks, hide server version, pin nginx:1.27-alpine - Fix health check endpoint path in backend Dockerfile - Use wget instead of curl for frontend health checks - Replace any types with proper types across backend and frontend - Replace || with ?? for config/form defaults - Replace hardcoded Tailwind colors with theme tokens - Replace console.log/error with logger in ErrorReporter and ChartErrorBoundary - Conditionally render AddToPlaylistModal to avoid hidden instances - Add NaN guards for parseInt in GameBrowseLayout - Standardize query key for publicSettings - Simplify README to Docker-first quickstart with wiki link - Fix license to GPL-3.0 in backend package.json - Slow down particle animation speed - Add Zod max-length limits on playlist/error schemas - Ensure positive limit in parsePaginationQuery - Validate settings category param against allowlist - Fix static/parameterized route ordering in favorites - Add asyncHandler to game-zip wildcard route - Close readline/stream on error in getRecentErrors - Add .catch() on fire-and-forget syncAndReload - Fix LazyBackgroundImage cleanup on unmount - Store NetworkStatusIndicator timeout in ref - Add Flash/SWF condition to random game query
126 lines
5.5 KiB
Plaintext
126 lines
5.5 KiB
Plaintext
# Nginx configuration for Flashpoint Web Frontend
|
|
# Serves static files and handles SPA routing
|
|
|
|
server {
|
|
listen 8080;
|
|
server_name localhost;
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# Gzip compression for better performance
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_proxied any;
|
|
gzip_comp_level 6;
|
|
gzip_types text/plain text/css text/xml text/javascript
|
|
application/json application/javascript application/xml+rss
|
|
application/rss+xml font/truetype font/opentype
|
|
application/vnd.ms-fontobject image/svg+xml;
|
|
|
|
# Security headers
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
|
|
# Content Security Policy
|
|
# Using 'self' for connect-src and frame-src since nginx proxies all backend
|
|
# traffic through the same origin — no need for localhost references
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none';" always;
|
|
|
|
# Permissions Policy (formerly Feature-Policy)
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
|
|
# Backend API
|
|
location ^~ /api/ {
|
|
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 300s;
|
|
proxy_connect_timeout 75s;
|
|
}
|
|
|
|
# Game proxy/media passthrough (through backend)
|
|
location ^~ /proxy/ {
|
|
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 300s;
|
|
proxy_connect_timeout 75s;
|
|
}
|
|
|
|
# Game content proxy - routes to backend (game service is integrated)
|
|
location ^~ /game-proxy/ {
|
|
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 300s;
|
|
proxy_connect_timeout 75s;
|
|
}
|
|
|
|
# Game ZIP content - routes to backend (game service is integrated)
|
|
location ^~ /game-zip/ {
|
|
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 300s;
|
|
proxy_connect_timeout 75s;
|
|
}
|
|
|
|
# SPA routing - serve index.html for all routes
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
# Cache static assets with versioned filenames
|
|
# NOTE: Must repeat security headers — nginx add_header in location blocks
|
|
# replaces ALL parent-level add_header directives
|
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none';" always;
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
}
|
|
|
|
# Don't cache index.html to ensure updates are seen
|
|
# NOTE: Must repeat security headers — see comment above
|
|
location = /index.html {
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
|
add_header Pragma "no-cache" always;
|
|
add_header Expires "0" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none';" always;
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
}
|
|
|
|
# Error handling
|
|
error_page 404 /index.html;
|
|
error_page 500 502 503 504 /50x.html;
|
|
location = /50x.html {
|
|
root /usr/share/nginx/html;
|
|
}
|
|
}
|