Files
admin/cms-port-leak-fix.md
vitya 5f8b2dfaf7 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>
2026-05-19 18:43:03 +03:00

105 lines
13 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.
# 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).