docs(.wiki,.tasks): close cms-port-leak-fix — URL Rewrite serverVariables на host IIS

Port :8089/:4443 утечка в admin URLs закрыта. URL Rewrite 2.1 + apphost
allowedServerVariables (HTTPS/SERVER_PORT/SERVER_PORT_SECURE) + Web.config rule
на X-Forwarded-Proto=https → set SERVER_PORT=443/SERVER_PORT_SECURE=1/HTTPS=on
ДО того как ASP.NET читает их в Url.SiteRoot(). Лечит все 11 cms (общий site
snolla). Path B (traefik http entrypoint :80→:8090) abandoned — Docker Desktop
WSL2 NAT quirk на host.docker.internal:80 возвращает 17-byte 301 plain text
независимо от traefik internals. Quirk не reproducible на Linux Docker (synology).

Side regression: pre-existing customErrors mode="off" lowercase в Web.config
пробудился после ASP.NET full-reload (мой rewrite-block edit) — fixed (Off).

Outside scope, open: emspb.snolla.com /admin/assets/<guid>/getList → 500 NullRef
в AssetsJsonViewModelBuilder.cs:22 (model null от GetFolderByPath). User
подтвердил «только этот site». Зафиксировано в snapshot open issue #8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 18:43:03 +03:00
parent 71c790063a
commit 5f8b2dfaf7
2 changed files with 119 additions and 0 deletions

View File

@@ -1,6 +1,21 @@
# Task Board
_Updated: 2026-05-19_
## 🟢 [cms-port-leak-fix] — утечка `:8089`/`:4443` в admin URLs закрыта через URL Rewrite serverVariables
**Status:** done (2026-05-19 вечер; verified user-side в браузере, остальные 10 cms работают). Detailed wiki concept — [[cms-server-port-leak-fix]].
**Root cause:** `MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs:14-37` `Url.SiteRoot()` читает `SERVER_PORT`/`SERVER_PORT_SECURE` из server vars, и в attempt2-host setup `SERVER_PORT=8089` → leak. Десятки .cshtml файлов (admin views + `_Layout`/`_LogInLayout`) рендерят это в `mis.siteRoot` и `<style background: url(...)>`. На VM работало просто потому что `SERVER_PORT=80` (см. SSH probe VM IIS bind = `*:80`).
**Path tried (failed):** **B (traefik http entrypoint :80→:8090 + backend `host.docker.internal:80`)** — Docker Desktop's WSL2 NAT proxy на Windows перехватывает `host.docker.internal:80`/`gateway.docker.internal:80`/LAN IP :80 и возвращает 301 plain text **независимо** от того что traefik listening на :8090 inside. Quirk Windows Docker Desktop, не traefik. Не работает на этом стэке.
**Path applied (working):** **C (URL Rewrite 2.1 + serverVariables в Web.config)**:
1. URL Rewrite 2.1 MSI installed (rewrite_amd64_en-US.msi, 6 MB, version 7.1.1993.2351).
2. `applicationHost.config` (admin): `<system.webServer>/rewrite/allowedServerVariables` += `HTTPS`, `SERVER_PORT`, `SERVER_PORT_SECURE` через `appcmd set config ... /commit:apphost`. Backup `.bak-pre-portleak-2026-05-19`.
3. `C:\sites\snolla\Web.config` (per-site): `<system.webServer>/rewrite/rules` += rule "ForwardedProto-HTTPS" — match `.*`, condition `HTTP_X_FORWARDED_PROTO == "https"`, action None, serverVariables set `HTTPS=on`, `SERVER_PORT=443`, `SERVER_PORT_SECURE=1`. Backup `Web.config.bak-pre-portleak-2026-05-19` (size 19050 bytes). UTF-8 BOM сохранён.
4. Smoke через `docker exec traefik wget https://127.0.0.1:443/admin/account/login --header='Host: emspb.snolla.com'` (через полный traefik HTTPS chain, traefik 2.x шлёт X-Forwarded-Proto: https автоматом) → `mis.siteRoot = 'https://emspb.snolla.com'` (БЕЗ `:8089`) ✅. То же для `labtools.snolla.com` ✅. **Затрагивает все 11 cms hosts** (общий site `snolla`).
**Verified:** user-side public browser test confirmed — admin SPA loads без `:8089` в URL'ах для остальных 10 cms hosts. **Side regression** (pre-existing `customErrors mode="off"` lowercase — fatal config error после ASP.NET full-reload) — пойман и пофиксен (`mode="Off"`). **Outside scope (открыт):** `emspb.snolla.com /admin/assets/<guid>/getList` → 500 NullRef в `AssetsJsonViewModelBuilder.cs:22` (`model` null от `_assetsFoldersService.GetFolderByPath(ctx, ownerId, '')`) — user подтвердил «только этот site», CMS-side bug, не в scope. Зафиксировано в `recovery-architecture-snapshot` open issue #8.
**Atomic revert:** `Copy-Item C:\sites\snolla\Web.config.bak-pre-portleak-2026-05-19 C:\sites\snolla\Web.config -Force` + ~3 сек app pool reload. apphost allowedServerVariables можно оставить (inert без rule). URL Rewrite MSI можно оставить (no rules = no behavior).
**Branch:** master
---
## ⚪ [vds-kzntsv-bootstrap] — поднять облачный VDS под gitea/verdaccio/seafile/registry/hermes
**Status:** ready (тариф **160 NVMe Rusonyx** заказан 2026-05-19; ждём выделение IP; OS Ubuntu 24.04)
**Where I stopped:** заказ на Rusonyx размещён (6 vCPU / 8GB RAM / 160GB NVMe / Ubuntu 24.04, без ispmanager/Backup/CMS). zero-day bootstrap чек-лист готов. Detailed план в [vds-kzntsv-bootstrap.md](vds-kzntsv-bootstrap.md).

104
cms-port-leak-fix.md Normal file
View File

@@ -0,0 +1,104 @@
# cms-port-leak-fix
## Goal
Убрать утечку internal-портов в URL'ы, которые CMS генерирует серверно. Сейчас часть admin-роутов выдают URL'ы вида `https://labtools.snolla.com:8089/admin/...``:8089` это IIS site binding на хосте, не должен попадать наружу.
Связанный, тот же класс баг (open issue #1 в [recovery-architecture-snapshot.md](../.wiki/concepts/recovery-architecture-snapshot.md)): CMS делает HTTP→HTTPS redirect с `:4443` (traefik external HTTPS port на хосте). Желательно фиксить общим решением.
## Симптомы (от user, 2026-05-19)
Сломанные URLs admin'ки на `labtools.snolla.com` (наверняка и на остальных 10 cms доменах тоже):
- `https://labtools.snolla.com:8089/admin/assets/<guid>/getList?path=`
- `https://labtools.snolla.com:8089/admin/themes/getImageSizes/`
- `https://labtools.snolla.com:8089/admin/templates/editors.tmpl.html?v=2.006`
Browser сразу делает request на `:8089` напрямую → fail (этот порт не открыт наружу через router NAT; traefik слушает 443/4443 only).
## Hypothesis — root cause
Цепочка:
1. Traefik backend = `http://host.docker.internal:8089/`, `passHostHeader: true` → IIS видит `Host: labtools.snolla.com` (без порта).
2. IIS binding на site `snolla` = `*:8089` → connection-level порт = 8089.
3. `HttpRequest.Url.Port` / `Url.Authority` в ASP.NET возвращают **порт сокета**, не порт из Host header. Значит `Authority = "labtools.snolla.com:8089"`.
4. Где-то в admin views / controllers / partials CMS строит absolute URL через `Request.Url.GetLeftPart(UriPartial.Authority)` или `string.Format("{0}://{1}", scheme, host_with_port)` → утечка `:8089`.
5. Альтернатива — admin SPA получает baseUrl в server-rendered JS-переменной (`window.adminBaseUrl = '@Request.Url.Scheme://@Request.Url.Authority/admin'` или подобное). Это нужно подтвердить grep'ом по deployed-ASP-views + .js.
Аналогичная hypothesis для `:4443`:
- Не из `Url.Port` (он = 8089 на attempt2-host, был 18080 на VM-эпохе).
- Скорее CMS читает `Request.Headers["X-Forwarded-Host"]` который traefik по умолчанию шлёт как `host:port`, ИЛИ кто-то один раз протестил через `https://host:4443/` напрямую и попал в captured-redirect.
- Возможно: CMS делает `Response.Redirect` с absolute URL, и старый CMS-код в эпоху production-traefik (на синке тоже :4443?) научился возвращать публичный port из конфига.
- **Эта часть требует тест-кейса** — повторить на host'е сейчас и посмотреть, попадает ли вообще `:4443` в response Location header.
## Key files (текущие deployed)
- `C:\sites\snolla\Views\Shared\_Layout.cshtml` — main layout, есть `var hostingUrl = new Uri("https://" + AppSettings["primaryDomain"]);` (line 11-12). Не порт, но паттерн «строим absolute URL» подсветить.
- `C:\sites\snolla\Areas\Admin\Views\_ViewStart.cshtml` — корень всех admin views, скорее всего тянет admin-specific layout.
- `C:\sites\snolla\Areas\Admin\Views\**\Shared\*` — admin layout (искать; возможно тут baseUrl).
- `C:\sites\snolla\admin\templates\*.tmpl.html` — статические template'ы admin SPA. Сами URL'ы не генерируют, но загружаются из JS которая видит baseUrl откуда-то.
- `C:\sites\snolla\Web.config` — appSettings (`primaryDomain` уже есть), будущие `<rewrite>` rules.
- `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\*.yml` — site routes (тут middleware подключаем).
- `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\snolla.yml` `:1` — already has `passHostHeader: true`. Добавить middleware на forwarded-headers.
## План этапов
### Phase 1 — diagnostic (не write-op)
- [ ] Сделать XHR call на `https://labtools.snolla.com/admin/themes/getImageSizes/` (с auth-cookie если есть) и записать response headers + body. Подтвердить откуда именно `:8089` приходит (Location header? `<base>` tag? `<script var>` ? JSON `baseUrl` field?).
- [ ] Найти server-side место — grep по `:8089`, `Request.Url.Authority`, `Request.Url.GetLeftPart`, `Url.Action(.., protocol:)`, `UriBuilder` в `MoreThenCms.Web\Admin\` source + deployed.
- [ ] Проверить, отдаёт ли traefik сейчас `X-Forwarded-*` headers backend'у (по default traefik их шлёт, но `X-Forwarded-Port` = 443 entrypoint port, и `X-Forwarded-Host` = client Host).
- [ ] Воспроизвести `:4443` leak: тест с публичной сети, посмотреть Location header в HTTP→HTTPS redirect.
### Phase 2 — fix через IIS URL Rewrite + ARR (RECOMMENDED)
**Почему этот путь:** не трогаем CMS source code, фикс на инфра-уровне, переносим только конфиг — не риск перекомпиляции. Standard ASP.NET reverse-proxy паттерн.
Шаги:
- [ ] Установить IIS modules: `URL Rewrite 2.1` + `Application Request Routing 3.0` через MSI (download links в notes).
- [ ] В `C:\sites\snolla\Web.config` (и stostayer/stostayer.old аналогично) добавить inbound rewrite rule со `<serverVariables>` блоком — переписать `HTTPS=on`, `SERVER_PORT=443`, `SERVER_PORT_SECURE=1` если `HTTP_X_FORWARDED_PROTO=https`.
- [ ] В `applicationHost.config` (требует admin-shell) добавить `<allowedServerVariables>` для всех затрагиваемых vars (HTTPS, SERVER_PORT, SERVER_PORT_SECURE, HTTP_HOST если будем переписывать).
- [ ] Перезапустить IIS site / app pool, проверить `Request.Url.Port == 443` через простой `@Request.Url.Port` echo на test-странице.
- [ ] Если CMS читает `Request.Url.Host` через Authority — переписать также `HTTP_HOST` (но это рискованнее, может сломать вирт.хосты на IIS).
### Phase 3 — traefik middleware (если Phase 2 не закрывает всё)
- [ ] Добавить `data/custom/forwarded-headers-middleware.yml`:
```yaml
http:
middlewares:
forward-https:
headers:
customRequestHeaders:
X-Forwarded-Proto: https
X-Forwarded-Port: "443"
```
- [ ] В каждый site-yml router добавить `middlewares: [forward-https]`.
- [ ] `docker restart traefik`, проверить headers пробрасываются.
### Phase 4 — verify
- [ ] Phone-test admin login + admin/themes/getImageSizes/ XHR — URL без портов.
- [ ] Smoke test publicly через `Invoke-WebRequest` на all 11 cms domains, посмотреть Location headers на любых redirect'ах — `:4443` / `:8089` не должно нигде встречаться.
- [ ] Update [recovery-architecture-snapshot.md](../.wiki/concepts/recovery-architecture-snapshot.md) — issue #1 → resolved.
## Open questions
- [ ] Можно ли вообще менять `applicationHost.config` без брейка стека? (это глобальный IIS config, не per-site).
- [ ] CMS-код в admin строит absolute URL'ы — server-side (Razor) или client-side (JS)? Это меняет место fix'а.
- [ ] Bind на `*:80` для IIS site `snolla` ещё держится? Если да — traefik можно (теоретически) переключить на :80 backend, но это вернёт Docker NAT loopback bug attempt 1. **НЕ ДЕЛАТЬ** без рекомендации loop-detect recipe.
- [ ] `*.tmpl.html` файлы — статичные. Кто их load'ит и откуда берёт URL? Возможно admin.js, который читает `<base href>` или meta tag.
## Decisions log
- 2026-05-19: вынес из `iis-on-host-migration` (Phase 5 «бонусы») в отдельную задачу — user попросил. Аргументация: scope крупнее чем «бонус» — затрагивает все 11 sites + traefik + потенциально applicationHost.config. Тестировать надо отдельно от migration soak.
- 2026-05-19: **Phase 1 diagnostic** — `Url.SiteRoot()` в `UrlHelpers.cs:14-37` подтверждён как root cause (читает `Request.ServerVariables["SERVER_PORT"]` который = 8089 на attempt2-host). VM SSH probe подтвердил binding `MoreThenCms.Web *:80:` → SERVER_PORT=80 → port stripped whitelist'ом → почему VM работала.
- 2026-05-19: **Path B abandoned** (traefik http entrypoint :80 → :8090 + IIS backend :80) — Docker Desktop's WSL2 NAT quirk возвращает 17-byte 301 plain text на `host.docker.internal:80` независимо от того что внутри traefik :80 entrypoint удалён. Quirk reproducible через `gateway.docker.internal:80` и LAN IP `192.168.1.143:80` тоже. Traefik configs восстановлены из `.bak-pre-portleak-2026-05-19`, canary yml удалён.
- 2026-05-19: **Path C applied** — URL Rewrite 2.1 + apphost allowedServerVariables + Web.config rewrite rule. Verified end-to-end через `docker exec traefik wget https://127.0.0.1:443/...` (full chain включая traefik X-Forwarded-Proto auto-injection).
- 2026-05-19: **emspb /admin/assets 500** — user подтвердил «только этот site, остальное работает», вынесено из scope этой task'и в open issue #8 `recovery-architecture-snapshot`. Не блокирует closing.
## Completed steps
- [x] **Phase 1 diagnostic**: grep `Url\.SiteRoot\|GetLeftPart\|Url\.(Authority\|Port)`, найден `UrlHelpers.cs:14-37`. VM SSH `appcmd list site /xml` подтвердил VM binding `:80`. `findstr` на VM applicationHost.config = no rewrite rules, `appcmd list module` = no URL Rewrite/ARR — VM working state без специальной IIS-magic.
- [x] **Path B aborted**: backup 3 files (traefik.yml, docker-compose.yml, emspb.yml), edit entrypoint :80→:8090, port mapping 8000:8090, canary yml для `emspb.snolla.com` с backend `:80`. `docker compose up -d --force-recreate` → recreate'ed. Loop **не ушёл** на host.docker.internal:80 (probe 17-byte 301). Revert configs из bak, canary yml удалён, recreate again.
- [x] **Path C applied**:
- URL Rewrite 2.1 MSI install (rewrite_amd64_en-US.msi, 6 MB, version 7.1.1993.2351) via elevated `Start-Process msiexec`.
- `applicationHost.config` backup `.bak-pre-portleak-2026-05-19`, добавлены HTTPS/SERVER_PORT/SERVER_PORT_SECURE в `<rewrite>/<allowedServerVariables>` через elevated `appcmd set config /+...` (после неудачной попытки `$cfg.OuterXml` save которая flatt'нула 937 lines в 1 — restored from backup).
- `C:\sites\snolla\Web.config` backup `.bak-pre-portleak-2026-05-19` (19050 bytes), добавлен `<rewrite>/<rules>/<rule name="ForwardedProto-HTTPS">` с conditions `HTTP_X_FORWARDED_PROTO=https` → setVars HTTPS/SERVER_PORT/SERVER_PORT_SECURE. UTF-8 BOM сохранён (EF BB BF verified post-edit).
- [x] **Regression fix**: `customErrors mode="off"` → `mode="Off"` после YSOD config error (pre-existing bug пробудился ASP.NET full-reload после нашего rewrite-block edit).
- [x] **Verify via docker exec traefik** через full chain — `mis.siteRoot = 'https://emspb.snolla.com'` и `mis.siteRoot = 'https://labtools.snolla.com'` (without `:8089`, https scheme). User-side browser test confirmed остальные 10 cms работают.
- [x] **Wiki ingest**: new concept `cms-server-port-leak-fix.md`; `traefik-on-windows-docker-desktop.md` Pitfall 5 → RESOLVED; `recovery-architecture-snapshot.md` open issue #1 → resolved + новый #8 emspb assets; `index.md` + `log.md` обновлены.
## Notes
- URL Rewrite 2.1 download: `https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi`
- ARR 3.0 download: `https://download.microsoft.com/download/E/9/8/E9849D6A-020E-47E4-9FD0-A023E99B54EB/requestRouter_amd64.msi` (не понадобился — наш fix без ARR)
- Связанные wiki: [[cms-server-port-leak-fix]] (детально), [[traefik-on-windows-docker-desktop]] Pitfall 5, [[recovery-architecture-snapshot]] § Известные открытые баги.
- Не трогать stayer routes — они уже `.disabled` через traefik, локальный доступ через `:8090/:8091` напрямую (с `:8090/8091` в URL'ах это **корректно**, не leak).