From db1c1eaab83b871fa8d8c6a98a0d0388eb6ecb4f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:34:20 +0000 Subject: [PATCH] fix(web): align nginx security rules with Nextcloud 35 reference Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/web/nextcloud.conf | 12 ++++++++++-- tests/web/nginx-dns-recovery.bats | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.docker/web/nextcloud.conf b/.docker/web/nextcloud.conf index 39ef1fd..9c99341 100644 --- a/.docker/web/nextcloud.conf +++ b/.docker/web/nextcloud.conf @@ -1,3 +1,6 @@ +# Nextcloud nginx configuration — root installation +# Version 2026-06-09 (official baseline) +# Docker adaptation: TLS terminates at nginx-proxy; PHP-FPM is reached via Docker DNS. upstream php-handler { zone php-handler 64k; resolver 127.0.0.11 valid=10s ipv6=off; @@ -39,7 +42,8 @@ server { # will add the domain to a hardcoded list that is shipped # in all major browsers and getting removed from this list # could take several months. - add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always; + # TLS terminates at nginx-proxy, which already adds the HSTS header. + #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # set max upload size and increase upload timeout: client_max_body_size 20G; @@ -87,7 +91,7 @@ server { include mime.types; types { text/javascript mjs; - application/wasm wasm; + #application/wasm wasm; # Already defined in nginx:alpine mime.types. } # Specify how to handle directories -- specifying `/index.php$request_uri` @@ -138,6 +142,10 @@ server { location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } + # Hide metadata files which would otherwise be served as plain files and + # leak dependency information (composer.json, package.json, core/shipped.json). + location ~ ^/(?:composer\.(?:json|lock)|package(?:-lock)?\.json|core/shipped\.json)$ { return 404; } + # Ensure this block, which passes PHP files to the PHP process, is above the blocks # which handle static assets (as seen below). If this block is not declared first, # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php` diff --git a/tests/web/nginx-dns-recovery.bats b/tests/web/nginx-dns-recovery.bats index 44c38ea..20c4093 100644 --- a/tests/web/nginx-dns-recovery.bats +++ b/tests/web/nginx-dns-recovery.bats @@ -13,6 +13,10 @@ setup() { mkdir -p "$workdir/htdocs" "$workdir/includes" chmod 755 "$workdir" printf '%s\n' " "$workdir/htdocs/status.php" + for file in composer.json composer.lock package.json package-lock.json core/shipped.json; do + mkdir -p "$workdir/htdocs/$(dirname "$file")" + printf '%s\n' '{"fixture":true}' > "$workdir/htdocs/$file" + done # Docker only permits --ip on networks with a user-configured subnet. # Let Docker select an available subnet instead of hardcoding an address. @@ -85,3 +89,22 @@ assert_healthy() { return 1 } } + +@test "nginx denies direct access to dependency and shipped metadata" { + start_app + docker run -d --name "$web" --network "$network" \ + --volume "$workdir/htdocs:/var/www/html:ro" \ + --volume "$workdir/includes:/etc/nginx/conf.d/includes:ro" \ + "$WEB_IMAGE" >/dev/null + + assert_healthy + for file in composer.json composer.lock package.json package-lock.json core/shipped.json; do + run docker exec "$web" wget -S -O /dev/null "http://127.0.0.1/$file" + [ "$status" -ne 0 ] || { echo "$file was served" >&2; return 1; } + [[ "$output" == *"404 Not Found"* ]] || { echo "$file: $output" >&2; return 1; } + done + + run docker exec "$web" nginx -t + [ "$status" -eq 0 ] || { echo "$output" >&2; return 1; } + [[ "$output" != *"duplicate extension"* ]] || { echo "$output" >&2; return 1; } +}