From c2787446c019458d635a152e113dc0f265c31f52 Mon Sep 17 00:00:00 2001 From: Leitwolf11 <238872178+Leitwolf11@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:26:55 +0300 Subject: [PATCH 1/2] Answer unknown paths of the tala.run sites with 404 Nginx sent every path that is not a file to index.html with 200, so probes such as /wp-admin/install.php, xmlrpc.php and phpinfo.php were answered as pages: scanners take that for a hit, and search engines index soft 404s. The three server blocks now fall back to @application, which serves the app shell only for the routes of the application, / and /docs/, and answers every other path with 404. error_page keeps index.html as the body of the 404, so a visitor still reaches the landing page through the router. A test reads the three sitemaps and requires every listed page to match the accepted routes, and scanner paths, /docs and nested paths not to; it also requires the same route rule in all three blocks. OPERATIONS.md describes the expected 404s, and asks for --no-same-owner, chown -R root:root and LF line endings when files are copied from a developer machine: the 2026-10-18 release had left the frontend owned by a Windows user ID. Applied on the server with the owner's approval and verified: nginx -t is clean; /, /docs/overview, talac /docs/command-line, tazel /docs/commands, robots.txt and /api/v1/site answer 200; /wp-admin/install.php, /no-such-page and /docs answer 404. npm test passes, 45 tests. --- Documents/OPERATIONS.md | 11 ++++-- frontend/tests/site-integrity.test.mjs | 20 +++++++++++ ops/nginx/tala.conf | 48 ++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/Documents/OPERATIONS.md b/Documents/OPERATIONS.md index f486d13..d77eb61 100644 --- a/Documents/OPERATIONS.md +++ b/Documents/OPERATIONS.md @@ -151,7 +151,11 @@ renaming: run the local gates. 2. On the server, copy `current` to a new directory next to it with `cp -a`, which keeps owners, modes and SELinux labels, and replace the - changed parts inside the copy. + changed parts inside the copy. Unpack archives with `--no-same-owner` and + run `chown -R root:root` on the replaced parts: an archive made on a + developer machine carries that machine's user and group IDs, which name + no account on the server, or a different one. Install Nginx and systemd + files with LF line endings. 3. Start the new API on a free loopback port as the service user with `PORT= dotnet site-api/Progmasoft.Tala.Website.Api.dll`, request `/health` and `/api/v1/site`, and stop it. @@ -237,7 +241,10 @@ boundary is clear. ## Monitoring and privacy The main language routes answer 200, and `/api/v1/site` returns only the -website's non-sensitive state. Expected forum responses are 200 for the five +website's non-sensitive state. The three sites answer 200 only for files and +for the routes of the application, `/` and `/docs/`; every other path, +such as a scanner's `/wp-login.php`, answers 404, with the application as +the body so that a visitor still reaches the landing page. Expected forum responses are 200 for the five static files and 404 for other paths. Expected legacy redirect responses are 308. In maintenance mode the main domain answers 503 with `Retry-After`. A 200 from a retired login or registry route is a regression. diff --git a/frontend/tests/site-integrity.test.mjs b/frontend/tests/site-integrity.test.mjs index 51a422b..2785a67 100644 --- a/frontend/tests/site-integrity.test.mjs +++ b/frontend/tests/site-integrity.test.mjs @@ -158,6 +158,26 @@ test("maintenance mode preserves the legacy redirect without opening the languag assert.match(live, /location \^~ \/api\/ \{\s*return 404;/); }); +test("only the routes of the application get the app shell; other paths answer 404", () => { + const live = readFileSync(join(websiteRoot, "ops/nginx/tala.conf"), "utf8"); + assert.doesNotMatch(live, /try_files \$uri \$uri\/ \/index\.html;/); + assert.equal(live.match(/try_files \$uri \$uri\/ @application;/g)?.length, 3); + assert.equal(live.match(/error_page 404 \/index\.html;/g)?.length, 3); + const patterns = [...live.matchAll(/if \(\$uri !~ "([^"]+)"\)/g)].map((match) => match[1]); + assert.equal(new Set(patterns).size, 1, "the three sites must accept the same routes"); + const route = new RegExp(patterns[0]); + const pages = ["public/sitemap.xml", "public/hosts/talac/sitemap.xml", "public/hosts/tazel/sitemap.xml"] + .flatMap(locations) + .map((url) => new URL(url).pathname); + assert.ok(pages.length > 3); + for (const path of pages) { + assert.match(path, route, `${path} is in a sitemap but would answer 404`); + } + for (const path of ["/wp-login.php", "/wp-admin/install.php", "/docs", "/docs/", "/docs/a/b", "/.env", "/a"]) { + assert.doesNotMatch(path, route, `${path} would be served as a page`); + } +}); + test("the former domain lands on the same path of tala.run and tala.run has no ViGet host", () => { const live = readFileSync(join(websiteRoot, "ops/nginx/tala.conf"), "utf8"); const forum = readFileSync(join(websiteRoot, "ops/nginx/forum.conf"), "utf8"); diff --git a/ops/nginx/tala.conf b/ops/nginx/tala.conf index 7f67bce..83a2652 100644 --- a/ops/nginx/tala.conf +++ b/ops/nginx/tala.conf @@ -93,9 +93,23 @@ server { location / { # Vue Router uses history URLs; real files still win before the app shell. - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @application; } + # The application has two kinds of routes: the landing page and + # /docs/. Any other path that is not a file is answered with 404, so + # that unknown addresses, including the probes for /wp-login.php and + # friends, are not served as pages. The body of the 404 is still the + # application, whose router takes a visitor to the landing page. + location @application { + if ($uri !~ "^/(docs/[a-z0-9-]+/?)?$") { + return 404; + } + rewrite ^ /index.html break; + } + + error_page 404 /index.html; + location ~ /\. { deny all; } @@ -141,9 +155,23 @@ server { } location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @application; + } + + # The application has two kinds of routes: the landing page and + # /docs/. Any other path that is not a file is answered with 404, so + # that unknown addresses, including the probes for /wp-login.php and + # friends, are not served as pages. The body of the 404 is still the + # application, whose router takes a visitor to the landing page. + location @application { + if ($uri !~ "^/(docs/[a-z0-9-]+/?)?$") { + return 404; + } + rewrite ^ /index.html break; } + error_page 404 /index.html; + location ~ /\. { deny all; } @@ -187,9 +215,23 @@ server { } location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @application; + } + + # The application has two kinds of routes: the landing page and + # /docs/. Any other path that is not a file is answered with 404, so + # that unknown addresses, including the probes for /wp-login.php and + # friends, are not served as pages. The body of the 404 is still the + # application, whose router takes a visitor to the landing page. + location @application { + if ($uri !~ "^/(docs/[a-z0-9-]+/?)?$") { + return 404; + } + rewrite ^ /index.html break; } + error_page 404 /index.html; + location ~ /\. { deny all; } From 1a82704248753633d1d546e59a21efda9d07e96b Mon Sep 17 00:00:00 2001 From: Leitwolf11 <238872178+Leitwolf11@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:07:01 +0300 Subject: [PATCH 2/2] Update vite and launch-editor for the npm audit and format the route test npm audit fix moves the development dependencies vite to 7.3.7 and launch-editor past GHSA-698v-4r3x-3vcv, GHSA-r825-w8mm-5pxw and GHSA-587h-7wmj-h5pm; the frontend audit gate failed on them. Prettier formats tests/site-integrity.test.mjs, which the format check flagged. --- frontend/package-lock.json | 6 +++--- frontend/tests/site-integrity.test.mjs | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 32fbef4..f6fc691 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2007,9 +2007,9 @@ } }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/tests/site-integrity.test.mjs b/frontend/tests/site-integrity.test.mjs index 2785a67..21e4ed2 100644 --- a/frontend/tests/site-integrity.test.mjs +++ b/frontend/tests/site-integrity.test.mjs @@ -166,14 +166,26 @@ test("only the routes of the application get the app shell; other paths answer 4 const patterns = [...live.matchAll(/if \(\$uri !~ "([^"]+)"\)/g)].map((match) => match[1]); assert.equal(new Set(patterns).size, 1, "the three sites must accept the same routes"); const route = new RegExp(patterns[0]); - const pages = ["public/sitemap.xml", "public/hosts/talac/sitemap.xml", "public/hosts/tazel/sitemap.xml"] + const pages = [ + "public/sitemap.xml", + "public/hosts/talac/sitemap.xml", + "public/hosts/tazel/sitemap.xml", + ] .flatMap(locations) .map((url) => new URL(url).pathname); assert.ok(pages.length > 3); for (const path of pages) { assert.match(path, route, `${path} is in a sitemap but would answer 404`); } - for (const path of ["/wp-login.php", "/wp-admin/install.php", "/docs", "/docs/", "/docs/a/b", "/.env", "/a"]) { + for (const path of [ + "/wp-login.php", + "/wp-admin/install.php", + "/docs", + "/docs/", + "/docs/a/b", + "/.env", + "/a", + ]) { assert.doesNotMatch(path, route, `${path} would be served as a page`); } });