diff --git a/backend/tests/architecture-tests.js b/backend/tests/architecture-tests.js index 8664f92..c2aef78 100644 --- a/backend/tests/architecture-tests.js +++ b/backend/tests/architecture-tests.js @@ -78,12 +78,17 @@ test('前端 package.json 与 lock 文件依赖同步且没有手工缓存版本 test('生产部署统一使用 Vite dist 且不会删除 lock 文件', () => { const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8'); const nginxConfig = fs.readFileSync(path.join(projectRoot, 'nginx', 'nginx.conf'), 'utf8'); + const nginxExample = fs.readFileSync(path.join(projectRoot, 'nginx', 'nginx.conf.example'), 'utf8'); assert.ok(installer.includes('npm ci --include=dev')); assert.ok(installer.includes('npm run build')); assert.ok(installer.includes('frontend/dist')); assert.strictEqual(/rm\s+-rf\s+node_modules\s+package-lock\.json/.test(installer), false); assert.ok(nginxConfig.includes('root /usr/share/nginx/html;')); assert.ok(nginxConfig.includes('alias /runtime/downloads/;')); + for (const config of [installer, nginxConfig, nginxExample]) { + assert.ok(config.includes('proxy_hide_header X-Request-ID;')); + assert.ok(config.includes('proxy_hide_header Content-Security-Policy;')); + } }); test('Docker 构建上下文排除依赖、环境配置和运行时数据', () => { diff --git a/install.sh b/install.sh index 624155b..933961f 100644 --- a/install.sh +++ b/install.sh @@ -2407,6 +2407,16 @@ server { add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" 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: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; + # 对外安全头由 Nginx 统一输出,避免与后端重复 + proxy_hide_header X-Frame-Options; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header X-XSS-Protection; + proxy_hide_header Referrer-Policy; + proxy_hide_header Permissions-Policy; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Request-ID; + proxy_hide_header Content-Security-Policy; + # 隐藏Nginx版本号 server_tokens off; @@ -2707,6 +2717,16 @@ server { add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" 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: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; + # 对外安全头由 Nginx 统一输出,避免与后端重复 + proxy_hide_header X-Frame-Options; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header X-XSS-Protection; + proxy_hide_header Referrer-Policy; + proxy_hide_header Permissions-Policy; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Request-ID; + proxy_hide_header Content-Security-Policy; + # 隐藏Nginx版本号 server_tokens off; @@ -2857,6 +2877,16 @@ server { add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + # 对外安全头由 Nginx 统一输出,避免与后端重复 + proxy_hide_header X-Frame-Options; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header X-XSS-Protection; + proxy_hide_header Referrer-Policy; + proxy_hide_header Permissions-Policy; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Request-ID; + proxy_hide_header Content-Security-Policy; + # 隐藏Nginx版本号 server_tokens off; @@ -3621,6 +3651,35 @@ update_migrate_nginx_config() { mv "$secured_file" "$config_file" fi + if ! grep -q "proxy_hide_header X-Request-ID" "$config_file"; then + local proxy_headers_file="${config_file}.proxy-headers.$$" + if ! awk ' + /add_header Content-Security-Policy/ && !inserted_proxy_headers { + print + print "" + print " # 对外安全头由 Nginx 统一输出,避免与后端重复" + print " proxy_hide_header X-Frame-Options;" + print " proxy_hide_header X-Content-Type-Options;" + print " proxy_hide_header X-XSS-Protection;" + print " proxy_hide_header Referrer-Policy;" + print " proxy_hide_header Permissions-Policy;" + print " proxy_hide_header Strict-Transport-Security;" + print " proxy_hide_header X-Request-ID;" + print " proxy_hide_header Content-Security-Policy;" + inserted_proxy_headers = 1 + next + } + { print } + END { if (!inserted_proxy_headers) exit 2 } + ' "$config_file" > "$proxy_headers_file"; then + rm -f "$proxy_headers_file" + print_error "无法为 Nginx 配置添加上游响应头去重: $config_file" + cp -a "$backup_dir/$backup_name" "$config_file" + return 1 + fi + mv "$proxy_headers_file" "$config_file" + fi + if ! grep -q "location /downloads/" "$config_file"; then local migrated_file="${config_file}.migrate.$$" awk -v project_dir="$PROJECT_DIR" ' @@ -3638,7 +3697,9 @@ update_migrate_nginx_config() { mv "$migrated_file" "$config_file" fi - if ! grep -q "root ${PROJECT_DIR}/frontend/dist;" "$config_file" || ! grep -q "location /downloads/" "$config_file"; then + if ! grep -q "root ${PROJECT_DIR}/frontend/dist;" "$config_file" || \ + ! grep -q "location /downloads/" "$config_file" || \ + ! grep -q "proxy_hide_header X-Request-ID" "$config_file"; then print_error "无法安全迁移 Nginx 配置: $config_file" cp -a "$backup_dir/$backup_name" "$config_file" return 1 diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 79cf393..f9a822e 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -13,6 +13,16 @@ server { add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" 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: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; + # 对外安全头由 Nginx 统一输出,避免与后端重复 + proxy_hide_header X-Frame-Options; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header X-XSS-Protection; + proxy_hide_header Referrer-Policy; + proxy_hide_header Permissions-Policy; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Request-ID; + proxy_hide_header Content-Security-Policy; + # 隐藏Nginx版本 server_tokens off; diff --git a/nginx/nginx.conf.example b/nginx/nginx.conf.example index 0e9f432..d8320f1 100644 --- a/nginx/nginx.conf.example +++ b/nginx/nginx.conf.example @@ -52,6 +52,16 @@ server { add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" 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: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; + # 对外安全头由 Nginx 统一输出,避免与后端重复 + proxy_hide_header X-Frame-Options; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header X-XSS-Protection; + proxy_hide_header Referrer-Policy; + proxy_hide_header Permissions-Policy; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Request-ID; + proxy_hide_header Content-Security-Policy; + # 隐藏 Nginx 版本 server_tokens off;