Files
admin/cms-server-port-leak-fix.md
vitya ec9177dddb docs(.wiki,.tasks): cms-admin-assets-root-folders-seed — DB seed 15 missing root rows
Admin /admin/assets/<siteId>/getList?path= крашился 500 NullReferenceException
в AssetsJsonViewModelBuilder.cs:22 (model.ParentPath на null) для 15 sites без
root AssetsFolder в Folders table (emspb.ru, pilorama98.ru, labtools.pro,
kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru,
profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru + 4 sites с NULL Domain).

Root создавался lazy при first upload — sites которые никогда не использовали
admin assets UI остались без root. Frontend (AssetsAppFunc.cs:66-86) делает
proper null-check → 404, только admin view-model builder упустил.

Fix: idempotent SQL seed (WHERE NOT EXISTS), 15 rows inserted. Inserted
FolderId/OwnerId captured в .tasks/...inserted-rows.txt для atomic revert.
Browser-verified user'ом на pilorama98/emspb.

Долгосрочный TODO: null-guard в AssetsJsonViewModelBuilder.Build (CMS code),
требует recompile DLL — отложено до восстановления build env (vds-kzntsv-bootstrap).
snapshot open issue #8 → resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:21:13 +03:00

175 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: CMS port-leak fix — URL Rewrite serverVariables на host IIS
type: concept
tags: [iis, url-rewrite, traefik, x-forwarded, asp-net, gotcha]
sources: [../sources/iis-host-migration-2026-05-19.md]
updated: 2026-05-19
---
# CMS port-leak fix
Решение для open issue #1 из [[recovery-architecture-snapshot]] (раньше «X-Forwarded headers не настроены, :4443 leak») и админ-utечки `:8089` обнаруженной 2026-05-19 вечером после attempt 2 host-IIS миграции. Документирует **root cause**, **почему VM работала**, **почему обходной path-B на Windows Docker Desktop невозможен**, и **что в итоге применено**.
Связано: [[traefik-on-windows-docker-desktop]] Pitfall 5, [[iis-host-migration-2026-05-19]] Phase 10, [[docker-host-loopback-detect]].
## Симптом
Admin URLs формата (после attempt 2 миграции на host-IIS:8089):
- `https://emspb.snolla.com:8089/admin/assets/<guid>/getList?path=`
- `https://emspb.snolla.com:8089/admin/themes/getImageSizes/`
- `https://emspb.snolla.com:8089/admin/templates/editors.tmpl.html?v=2.006`
Browser HSTS upgrade'ит `http://...:8089``https://...:8089` → TCP open, TLS handshake fails (8089 = plain HTTP) → admin SPA ломается.
Аналогично, ранее (issue #1) — CMS делал HTTP→HTTPS redirect с `:4443` (traefik external port).
## Root cause
`MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs:14-37` `Url.SiteRoot()`:
```csharp
var port = context.Request.ServerVariables["SERVER_PORT"];
if (usePort) {
if (port == null || port == "80" || port == "443") port = "";
else port = ":" + port;
}
var protocol = context.Request.ServerVariables["SERVER_PORT_SECURE"];
if (protocol == null || protocol == "0") protocol = "http://"; else protocol = "https://";
var sOut = protocol + context.Request.ServerVariables["SERVER_NAME"] + port + appPath;
```
Читает **socket-level** server variables. На IIS site `snolla` binding `*:8089` HTTP → `SERVER_PORT=8089`, `SERVER_PORT_SECURE=0` → формирует `http://emspb.snolla.com:8089` → рендерится в Razor:
```cshtml
@* C:\sites\snolla\Views\Shared\_Layout.cshtml:229 *@
mis.siteRoot = '@Url.SiteRoot().Replace("http:", "https:")' + '/admin';
@* и _LogInLayout.cshtml:156 *@
mis.siteRoot = '@Url.SiteRoot()';
```
Замена `http:→https:` в Layout была *прошлым* частичным patch'ем — не убирает порт. Десятки .cshtml в admin также используют `Url.SiteRoot()` для inline `<style background: url(... + "/images/loading2.gif")>` — все utекают `:8089`.
## Почему VM работала (без всяких rewrite)
SSH probe VM (parallel running snolla-recovery): `appcmd list site /xml``MoreThenCms.Web bindings="http/*:80:"`. **`SERVER_PORT=80`** → `port` пустеет (whitelist `{80,443}`), `mis.siteRoot = 'http://emspb.snolla.com'` (no port, http scheme). Browser HSTS upgrade'ит до https и попадает обратно через router-NAT-traefik-chain в backend. Работает.
VM `applicationHost.config` проверен — **никаких** rewrite rules, никакого URL Rewrite/ARR модуля. Production magic = просто IIS бинд на `:80`.
## Почему path B (host IIS :80 + traefik backend :80) НЕ работает на Windows Docker Desktop
Pre-flight 2026-05-19 вечер:
```
docker exec traefik wget -qS http://host.docker.internal:80/admin/account/login
→ HTTP/1.1 301 Moved Permanently
Content-Length: 17
Content-Type: text/plain
Location: https://emspb.snolla.com/admin/account/login
(НЕТ Server: header — не IIS, не traefik)
```
`Get-NetTCPConnection -LocalPort 80 -State Listen` → только `PID=4 NAME=System` (http.sys/IIS). Никакого другого listener'а. Direct `Invoke-WebRequest http://127.0.0.1/admin/...` с правильным Host header возвращает 200 OK с `Server: Microsoft-IIS/10.0`.
Но через **`host.docker.internal:80`**, **`gateway.docker.internal:80`** (192.168.65.1), **`192.168.1.143:80`** (LAN IP) — все три возвращают тот же 17-byte 301 plain text. То есть **Docker Desktop's WSL2 NAT proxy на Windows перехватывает container-→host:80 traffic и подменяет ответ HTTP→HTTPS redirect'ом**.
Подтверждение что **не** traefik: после смены traefik http entrypoint `:80 → :8090` (`docker-compose.yml ports: 8000:8090`, `traefik.yml http.address: ":8090"`) и `docker compose up -d --force-recreate``netstat -tlnp` в контейнере показывает `:8090 LISTEN`, **нет** `:80`. И всё равно `host.docker.internal:80` возвращает тот же 17-byte 301.
**Этого quirk'а нет на Linux Docker** (synology) — там `host.docker.internal` либо не существует, либо ведёт себя стандартно. Поэтому VM-стек работал, host-Windows-Docker — нет.
## Применённое решение (path C)
URL Rewrite 2.1 + `<serverVariables>` rule на host IIS — переписывает `SERVER_PORT/SERVER_PORT_SECURE/HTTPS` ДО того как ASP.NET их читает.
### Шаги
1. **MSI install URL Rewrite 2.1** (elevated):
```powershell
$msi = "$env:TEMP\rewrite_amd64_en-US.msi"
Invoke-WebRequest 'https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi' -OutFile $msi
Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart" -Verb RunAs -Wait
```
Verify: `Get-Item C:\Windows\System32\inetsrv\rewrite.dll` (FileVersion 7.1.1993.2351).
2. **applicationHost.config — `<allowedServerVariables>`** (elevated, через appcmd):
```powershell
$appcmd = 'C:\Windows\System32\inetsrv\appcmd.exe'
foreach ($n in 'HTTPS', 'SERVER_PORT', 'SERVER_PORT_SECURE') {
& $appcmd set config -section:system.webServer/rewrite/allowedServerVariables "/+[name='$n']" /commit:apphost
}
```
**Built-in IIS server variables** (HTTPS/SERVER_PORT/*) MUST be в apphost-allowedServerVariables, **не** site Web.config. Иначе HTTP 500 "URL Rewrite Module Error" без body.
3. **Site `C:\sites\snolla\Web.config`** (UTF-8 BOM!) — добавить в `<system.webServer>`:
```xml
<rewrite>
<rules>
<rule name="ForwardedProto-HTTPS" stopProcessing="false">
<match url=".*" />
<conditions>
<add input="{HTTP_X_FORWARDED_PROTO}" pattern="^https$" />
</conditions>
<serverVariables>
<set name="HTTPS" value="on" />
<set name="SERVER_PORT" value="443" />
<set name="SERVER_PORT_SECURE" value="1" />
</serverVariables>
<action type="None" />
</rule>
</rules>
</rewrite>
```
`<action type="None"/>` — rule не редиректит/переписывает URL, только сидит как side-effect ставящая server vars. `stopProcessing="false"` чтобы другие rules могли продолжить (на случай если добавим).
4. **Recycle** — IIS auto-recycle при изменении Web.config (~3 сек).
5. **Verify**:
```bash
docker exec traefik sh -c "wget -qS --no-check-certificate -O- \
--header='Host: emspb.snolla.com' \
https://127.0.0.1:443/admin/account/login 2>&1 | grep siteRoot"
# Expected: mis.siteRoot = 'https://emspb.snolla.com'; (БЕЗ :8089)
```
Traefik 2.x по-default шлёт `X-Forwarded-Proto: https` для requests через https entrypoint — наш `HTTP_X_FORWARDED_PROTO` condition fires автоматически.
### Atomic revert
```powershell
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)
# MSI URL Rewrite можно оставить (модуль без rule = no behavior)
```
## Gotchas
### `customErrors mode="off"` lowercase — fatal regression
`C:\sites\snolla\Web.config:43` имел `<customErrors mode="off" />` (lowercase). ASP.NET до моего edit'а **lazily** валидировал это — работало. После моего Web.config edit'а (добавил `<rewrite>` block) → ASP.NET full-reload → enum parse fail:
```
Значение свойства 'mode' не может быть проанализировано.
Ошибка: Значение перечисления должно быть одним из следующих: RemoteOnly, On, Off.
```
YSOD config error блокирует **весь** site. Fix: `mode="Off"` (capital O). Урок: при любом Web.config edit'е сначала grep на enum-style attributes на корректный casing.
### `[xml]` save в PowerShell flattens applicationHost.config
`$cfg.OuterXml` сбрасывает все пробелы/переносы → файл 937 строк превращается в 1 строку. IIS читает (whitespace для XML parser не важен), но файл нечитаем для людей. Backup и `appcmd set config /+...` — единственно правильный способ редактировать apphost programmatically.
### Site-level `<allowedServerVariables>` для built-in vars
Site `Web.config` MOJET содержать `<rewrite><allowedServerVariables>` ТОЛЬКО для custom variables (HTTP_X_*, etc). Для built-in IIS vars (HTTPS, SERVER_PORT, SERVER_PORT_SECURE) — apphost OR HTTP 500. Не дублировать в site-level — конфликт.
## Сделано на host'е (2026-05-19 вечер)
- `C:\Windows\System32\inetsrv\config\applicationHost.config` — `<rewrite>/<allowedServerVariables>` += HTTPS, SERVER_PORT, SERVER_PORT_SECURE. Backup `.bak-pre-portleak-2026-05-19`.
- `C:\sites\snolla\Web.config` — `<rewrite>/<rules>` += rule "ForwardedProto-HTTPS"; `customErrors mode="off"` → `"Off"`. Backup `.bak-pre-portleak-2026-05-19`.
- URL Rewrite 2.1 MSI installed.
- Затрагивает **все 11 cms hosts** (общий site `snolla`): emspb, labtools, labtoolspro, pilorama98, tandemmebel, kupimknigi, maljarka, sestech, isc-artmaterials, rimiz, plus snolla.com sub-domains.
## Sibling fix (отдельная task, тот же день)
После port-leak fix'а surfaced `/admin/assets/<guid>/getList → 500 NullReferenceException` на admin assets. Оказался **не site-specific** — общий для 15 sites без root AssetsFolder в DB. Решён DB seed'ом, см. [[cms-admin-assets-root-folder-seed]].