fix(utf8): закрыть lossy-границы конфигурации и API

This commit is contained in:
2026-09-07 17:25:27 +05:00
parent ab788725cf
commit af9f476658
38 changed files with 759 additions and 159 deletions
+16 -8
View File
@@ -117,10 +117,10 @@
| Вход | Ожидание |
| --- | --- |
| NUL, `U+FDD0`, `U+FDEF`, `U+FFFE`, `U+FFFF`, `U+1FFFF`, `U+10FFFF` | отказ ЗАПИСИ: `utf8_is_valid` отвергает такое значение, и EnvironmentFile не загрузится |
| NUL, `U+FEFF`, `U+FDD0`, `U+FDEF`, `U+FFFE`, `U+FFFF`, `U+1FFFF`, `U+10FFFF` | отказ ЗАПИСИ: публичный контракт EnvironmentFile запрещает такое значение |
| одиночный суррогат `U+D800` | отказ — иначе `TextEncoder` молча заменил бы его на `U+FFFD`, то есть подменил бы секрет |
| `U+FDCF`, `U+FDF0`, `U+FFFD`, `U+10FFFD`, `U+1F600` | принимаются: правило описывает диапазон, а не окрестность |
| `\n`, `\r`, `\t`, `U+007F`, `U+0085`, `U+FEFF` | формат их НЕСЁТ и round-trip сохраняет; запрещает их контракт учётных данных, а не транспорт |
| `\n`, `\r`, `\t`, `U+007F`, `U+0085` | формат их НЕСЁТ и round-trip сохраняет; запрещает их контракт учётных данных, а не транспорт |
### A4b. Непригодная конфигурация отвергается до первой мутации
@@ -591,18 +591,27 @@ wildcard-маршрутом фронтенда или дублирующая р
- управляющие символы Unicode целиком, то есть `Cc`: `\n`, `\r`, `\t`, `NUL`,
`DEL` и C1 (`U+0085`, `U+009F`). Раньше проверялись только C0 и DEL, а
документация обещала «без управляющих символов» — то есть была шире кода;
- значения вне домена systemd (`U+FDD0`, `U+FDEF`, `U+FFFE`, `U+FFFF`,
- значения вне документированного домена systemd (`U+FEFF`, `U+FDD0`, `U+FDEF`, `U+FFFE`, `U+FFFF`,
`U+1FFFF`, `U+10FFFF`, невалидный UTF-8) отвергаются: с ними
`/etc/hy2xs/hy2xs.env` не загрузится и юнит не стартует;
- соседи запрещённых диапазонов (`U+FDCF`, `U+FDF0`, `U+FFFD`) принимаются:
правило описывает множество systemd, а не окрестность подозрительных значений;
- `U+FEFF` отвергается **политикой продукта**, а не форматом:
`0xFEFF & 0xFFFE == 0xFEFE`, и systemd такое значение принимает. Отдельный
тест `TestEnvTransportDomainMatchesSystemd` закрепляет, что домен транспорта
не шире и не уже множества systemd, а
- `U+FEFF` отвергается **транспортным доменом**: публичная документация systemd
запрещает его, хотя реализация v257.13 случайно пропускает из-за маски
`0xFEFF & 0xFFFE == 0xFEFE`. Тест
`TestEnvTransportDomainMatchesDocumentedSystemdContract` закрепляет публичный контракт, а
`TestProductPolicyIsWiderThanTransportDomain` — что политика и домен остаются
различимы.
`orchestrator/test/strict-text-read.test.ts` подаёт reader'у реальные байтовые
последовательности `0xFF`, оборванную `0xC3` и `ED A0 80`. Ни одна из них не
превращается в U+FFFD; начальный BOM сохраняется как U+FEFF и доходит до
транспортного отказа.
`apps/controller/json_body_test.go` и HTTP-тест входа доказывают то же на API:
повреждённый UTF-8 и непарные `\uD800`/`\uDC00` отвергаются до стандартного
Go-декодера, а настоящий U+FFFD остаётся допустимым значением.
`apps/controller/validator_test.go` — ПРОВОДКА, а не контракт: теги
`credentialStr` и `adminPassword` прогоняются через production-валидатор и
обязаны отвечать так же, как функции контракта, на тех же граничных значениях.
@@ -932,4 +941,3 @@ Hysteria. Вторая не была замечена только потому,
Отсутствие legacy-маршрута доказывает не `grep` по исходникам, а
`TestRouterHasNoLegacyNamespace` на таблице маршрутов собранного роутера —
и существование этого теста само проверяется контрактом.
+2 -2
View File
@@ -55,7 +55,8 @@
35. **оператор входит в панель**: `POST /api/auth/login` с bootstrap-учётными данными из `/etc/hy2xs/bootstrap-admin.secret` отвечает `code: 20000` и непустым `accessToken`. Заведомо неверные учётные данные (со СЛУЧАЙНЫМ паролем, а не с литералом) дают HTTP 200 с конвертом отказа: `code: 50000`, причина `invalid_credentials` и отсутствие `accessToken`
36. пароль предельной длины, назначенный формой смены пароля, принимается формой входа: границы обеих форм совпадают с серверными. Границ **две** — 6-64 символа Unicode и не более 72 байт в UTF-8 (предел bcrypt): пароль из 36 кириллических букв (72 байта) принимается, из 37 (74 байта) — отвергается конвертом валидации, а не системной ошибкой
37. `HY2XS_ADMIN_INITIAL_PASSWORD` с пробелом по краям доезжает до учётной записи неизменным: значение записано в `hy2xs.env` в двойных кавычках, и вход выполняется ровно им, а не обрезанным
37a. `HY2XS_ADMIN_INITIAL_PASSWORD` со значением вне домена systemd (`U+FDD0`, `U+FFFF`, невалидный UTF-8) **роняет `preflight-install`** то есть отказ приходит до первой мутации хоста, а не после установки пакетов; сервер остаётся нетронутым
37a. `HY2XS_ADMIN_INITIAL_PASSWORD` со значением вне документированного домена systemd (`U+FEFF`, `U+FDD0`, `U+FFFF`) или файл с байтами `0xFF`, оборванной `0xC3`, `ED A0 80` **роняет `preflight-install`** — отказ приходит до первой мутации хоста; сервер остаётся нетронутым
37b. API входа и импорта отвергает повреждённый UTF-8 и непарные UTF-16 surrogate escape до `encoding/json`; настоящий U+FFFD работает как обычный символ
38. `last_login_at` администратора обновляется после успешного входа и не меняется после неудачной попытки
## C0. Панель обязана впускать, а не слушать порт
@@ -237,4 +238,3 @@ idle timeout проходил семантическую проверку. То
Го- и TS-санитайзеры описывают один контракт и покрыты зеркальными тестами:
граница определяется значением, а не именем ключа.