fix(utf8): закрыть lossy-границы конфигурации и API
This commit is contained in:
@@ -131,10 +131,9 @@ func IsValidAdminUsername(username string) bool {
|
||||
// же класс дефекта, ради уничтожения которого контракт и существует, только на
|
||||
// один слой ниже.
|
||||
//
|
||||
// U+FEFF в этот список НЕ входит: `0xFEFF & 0xFFFE` — это `0xFEFE`, и systemd
|
||||
// такое значение принимает (комментарий `/* BOM */` в его исходнике относится к
|
||||
// U+xFFFE и является неточностью самого systemd). Запрет U+FEFF — отдельное
|
||||
// продуктовое решение, см. hasForbiddenRune.
|
||||
// Публичная документация systemd также запрещает U+FEFF. Реализация v257.13
|
||||
// случайно пропускает его из-за маски в `unichar_is_valid`; продукт следует
|
||||
// документированному контракту, а не этому расхождению реализации.
|
||||
func isUnicodeNoncharacter(r rune) bool {
|
||||
if r >= 0xFDD0 && r <= 0xFDEF {
|
||||
return true
|
||||
@@ -144,8 +143,7 @@ func isUnicodeNoncharacter(r rune) bool {
|
||||
|
||||
// IsEnvTransportableText сообщает, переживёт ли значение файл окружения.
|
||||
//
|
||||
// Это ДОМЕН systemd, а не политика HY2XS: здесь перечислено ровно то, что
|
||||
// `utf8_is_valid` отвергает, и ничего сверх того. Продуктовые ограничения
|
||||
// Это документированный ДОМЕН systemd, а не политика HY2XS. Продуктовые ограничения
|
||||
// (границы длины, запрет управляющих символов) живут отдельно — иначе
|
||||
// невозможно объяснить, какое из двух правил нарушено и чем это грозит.
|
||||
func IsEnvTransportableText(value string) bool {
|
||||
@@ -159,7 +157,7 @@ func IsEnvTransportableText(value string) bool {
|
||||
for _, r := range value {
|
||||
// NUL — валидный UTF-8, но границей строки в C; systemd отвергает его
|
||||
// отдельной веткой (`embedded NUL`).
|
||||
if r == 0 {
|
||||
if r == 0 || r == 0xFEFF {
|
||||
return false
|
||||
}
|
||||
if isUnicodeNoncharacter(r) {
|
||||
@@ -178,7 +176,7 @@ func IsEnvTransportableText(value string) bool {
|
||||
// (IsEnvTransportableText);
|
||||
// 6-64 code points — граница, которую видит оператор;
|
||||
// <= 72 UTF-8 bytes — граница, которую ставит bcrypt;
|
||||
// Cc и U+FEFF — продуктовая политика, см. hasForbiddenRune.
|
||||
// Cc — продуктовая политика, см. hasForbiddenRune.
|
||||
//
|
||||
// Длина в СИМВОЛАХ, а не в байтах, потому что именно так её считает
|
||||
// go-playground/validator (`utf8.RuneCountInString` в baked_in.go) и так её
|
||||
@@ -226,22 +224,13 @@ func IsValidAdminPassword(password string) bool {
|
||||
// символов» — то есть документация была шире кода ровно на C1. Проверяется
|
||||
// теперь то, что обещано.
|
||||
//
|
||||
// И отдельно U+FEFF. Формат его несёт: `0xFEFF & 0xFFFE` — это `0xFEFE`, и
|
||||
// `unichar_is_valid` такое значение ПРИНИМАЕТ (комментарий `/* BOM */` в
|
||||
// исходнике systemd относится к U+xFFFE и является его собственной
|
||||
// неточностью). Запрет здесь — решение продукта: невидимый символ нулевой
|
||||
// ширины в пароле нельзя ни увидеть в поле ввода, ни повторить при следующем
|
||||
// входе, то есть он умеет ровно одно — запереть оператора снаружи. Ровно тем же
|
||||
// обоснованием запрещены и управляющие символы: перевод строки невозможно
|
||||
// ввести в однострочное поле формы входа.
|
||||
// U+FEFF здесь уже не проверяется: он принадлежит документированному домену
|
||||
// EnvironmentFile и отвергается IsEnvTransportableText.
|
||||
func hasForbiddenRune(value string) bool {
|
||||
for _, r := range value {
|
||||
if r < 0x20 || (r >= 0x7F && r <= 0x9F) {
|
||||
return true
|
||||
}
|
||||
if r == 0xFEFF {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -146,8 +146,7 @@ func TestAdminPasswordBoundaries(t *testing.T) {
|
||||
{"U+FDF0 рядом с диапазоном", "abcde" + string(rune(0xFDF0)), true},
|
||||
{"U+FFFD (replacement)", "abcde" + string(rune(0xFFFD)), true},
|
||||
|
||||
// U+FEFF формат несёт (0xFEFF & 0xFFFE == 0xFEFE), и отвергается он
|
||||
// продуктовой политикой, а не systemd.
|
||||
// U+FEFF отвергается документированным транспортным доменом systemd.
|
||||
{"U+FEFF", "abcde" + string(rune(0xFEFF)), false},
|
||||
}
|
||||
|
||||
@@ -187,18 +186,18 @@ func TestAdminPasswordLimitsAreIndependent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Домен транспорта повторяет множество systemd ТОЧНО — не шире и не уже.
|
||||
// Домен транспорта повторяет публично документированное множество systemd.
|
||||
//
|
||||
// Шире означало бы, что HY2XS отвергает значение, которое systemd загрузил бы;
|
||||
// уже — что значение проходит все двери продукта и делает файл окружения
|
||||
// незагружаемым, а юнит — не стартующим. Второе и было дефектом: контракт знал
|
||||
// про управляющие символы и не знал про noncharacters.
|
||||
// Шире означало бы собственный запрет сверх публичного контракта; уже — что
|
||||
// значение проходит все двери продукта вопреки документации EnvironmentFile.
|
||||
// Второе и было дефектом: контракт знал про управляющие символы и не знал про
|
||||
// noncharacters.
|
||||
//
|
||||
// Границы взяты из `unichar_is_valid` (systemd, src/basic/utf8.c) и проверяются
|
||||
// вместе с соседями: правило обязано отвергать ровно диапазон, а не окрестность.
|
||||
func TestEnvTransportDomainMatchesSystemd(t *testing.T) {
|
||||
func TestEnvTransportDomainMatchesDocumentedSystemdContract(t *testing.T) {
|
||||
rejected := []rune{
|
||||
0x0000, // embedded NUL — отдельная ветка utf8_is_valid
|
||||
0x0000, 0xFEFF, // NUL и BOM запрещены публичным контрактом
|
||||
0xFDD0, 0xFDEF, // U+FDD0..U+FDEF
|
||||
0xFFFE, 0xFFFF,
|
||||
0x1FFFE, 0x1FFFF,
|
||||
@@ -206,21 +205,20 @@ func TestEnvTransportDomainMatchesSystemd(t *testing.T) {
|
||||
}
|
||||
for _, r := range rejected {
|
||||
if IsEnvTransportableText(string(r)) {
|
||||
t.Errorf("U+%04X принят: systemd откажется загружать такой EnvironmentFile", r)
|
||||
t.Errorf("U+%04X принят вопреки документированному контракту EnvironmentFile", r)
|
||||
}
|
||||
}
|
||||
|
||||
accepted := []rune{
|
||||
0x0020, 0x007F, 0x0085, // управляющие формат НЕСЁТ; их запрещает политика продукта
|
||||
0xFDCF, 0xFDF0, // соседи зарезервированного диапазона
|
||||
0xFEFF, // 0xFEFF & 0xFFFE == 0xFEFE — systemd принимает
|
||||
0xFFFD, // replacement character — обычный символ
|
||||
0x10FFFD, // последний scalar value, не noncharacter
|
||||
0x1F600, // 😀
|
||||
}
|
||||
for _, r := range accepted {
|
||||
if !IsEnvTransportableText(string(r)) {
|
||||
t.Errorf("U+%04X отвергнут: правило шире множества systemd", r)
|
||||
t.Errorf("U+%04X отвергнут: правило шире документированного множества systemd", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +237,7 @@ func TestEnvTransportDomainMatchesSystemd(t *testing.T) {
|
||||
// одно из двух правил отсутствует.
|
||||
func TestProductPolicyIsWiderThanTransportDomain(t *testing.T) {
|
||||
// Управляющие символы формат несёт — их запрещает HY2XS.
|
||||
for _, r := range []rune{0x0A, 0x0D, 0x09, 0x7F, 0x85, 0x9F, 0xFEFF} {
|
||||
for _, r := range []rune{0x0A, 0x0D, 0x09, 0x7F, 0x85, 0x9F} {
|
||||
password := "abcde" + string(r)
|
||||
if !IsEnvTransportableText(password) && r != 0x00 {
|
||||
t.Errorf("U+%04X: ожидалось, что формат его несёт", r)
|
||||
@@ -248,6 +246,9 @@ func TestProductPolicyIsWiderThanTransportDomain(t *testing.T) {
|
||||
t.Errorf("U+%04X принят паролем вопреки политике продукта", r)
|
||||
}
|
||||
}
|
||||
if IsEnvTransportableText("abcde" + string(rune(0xFEFF))) {
|
||||
t.Error("U+FEFF принят вопреки документированному контракту EnvironmentFile")
|
||||
}
|
||||
}
|
||||
|
||||
// Пароль НЕ триммится нигде, включая сам контракт.
|
||||
|
||||
Reference in New Issue
Block a user