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

13 KiB
Raw Blame History

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): 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.

Почему этот путь: не трогаем 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:
    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 — 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 diagnosticUrl.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

  • 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.
  • 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.
  • 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).
  • Regression fix: customErrors mode="off"mode="Off" после YSOD config error (pre-existing bug пробудился ASP.NET full-reload после нашего rewrite-block edit).
  • 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 работают.
  • 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).