Безопасность: fail-closed auth (H1), доверенный прокси для IP (H2), CSRF на /order (M2)
По результатам security-аудита (docs, run-1). Три верифицированные находки. H1 (HIGH) — fail-open аутентификация. loadUsers() при ошибке чтения/парсинга users.json молча возвращал пустой стор → authOpen() считал это первичной настройкой → после удаления first-run файла весь /admin отдавался без входа. Теперь: постоянный маркер configured (пишется при создании первого админа и бэкфиллится при загрузке валидного стора); authOpen() открыт только на реально свежей системе; при пустом/битом сторе на настроенной системе — 503 и блокировка до восстановления файла (authLockedOut). Тест TestAuthFailClosed (4 сценария). H2 (HIGH) — интернет-слой доверял клиентскому X-Forwarded-For/X-Real-IP, что позволяло обойти лимит входа по IP и травить аудит. Теперь заголовкам доверяем только от доверенного прокси (по умолчанию loopback; расширяется BJ_TRUSTED_PROXIES), из XFF берём правый элемент. Исправлено и в Go-модуле (modules/lk/security.go), и в PHP-ретрансляторе. Тесты TestClientIPTrust (Go) и прогон логики PHP. M2 (MEDIUM) — POST /order (поручение на списание бумаг) не проверял CSRF, в отличие от /login и /impersonate. Добавлен csrfOK (форма уже несла токен). Тест TestOrderCSRF. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// H1: пустой/битый users.json на настроенной системе НЕ должен открывать админку.
|
||||
func TestAuthFailClosed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
oldUsers, oldMarker, oldStore, oldFailed := usersFile, configuredMarker, globalUsers, usersLoadFailed
|
||||
t.Cleanup(func() {
|
||||
usersFile, configuredMarker, globalUsers, usersLoadFailed = oldUsers, oldMarker, oldStore, oldFailed
|
||||
})
|
||||
usersFile = filepath.Join(dir, "users.json")
|
||||
configuredMarker = filepath.Join(dir, "configured")
|
||||
|
||||
reload := func() {
|
||||
usersLoadFailed = false
|
||||
globalUsers = loadUsers()
|
||||
}
|
||||
|
||||
// 1) свежая система: файла нет, маркера нет → открыто для первичной настройки
|
||||
reload()
|
||||
if !authOpen() || authLockedOut() {
|
||||
t.Fatalf("свежая система: ожидали open, got open=%v locked=%v", authOpen(), authLockedOut())
|
||||
}
|
||||
|
||||
// 2) корректный стор с админом → не открыто, не заблокировано, маркер создан
|
||||
if err := os.WriteFile(usersFile, []byte(`[{"login":"admin","role":"admin","active":true}]`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reload()
|
||||
if authOpen() || authLockedOut() {
|
||||
t.Fatalf("настроенная система: open=%v locked=%v", authOpen(), authLockedOut())
|
||||
}
|
||||
if !isConfigured() {
|
||||
t.Fatal("маркер настройки не создан при загрузке валидного стора")
|
||||
}
|
||||
|
||||
// 3) файл ПОВРЕЖДЁН (битый JSON) → fail-closed
|
||||
if err := os.WriteFile(usersFile, []byte(`{ не json`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reload()
|
||||
if authOpen() {
|
||||
t.Error("битый users.json открыл админку без входа — fail-open!")
|
||||
}
|
||||
if !authLockedOut() {
|
||||
t.Error("битый users.json должен блокировать админку")
|
||||
}
|
||||
|
||||
// 4) файл ПРОПАЛ, но система была настроена (маркер есть) → fail-closed
|
||||
os.Remove(usersFile)
|
||||
reload()
|
||||
if authOpen() {
|
||||
t.Error("пропавший users.json на настроенной системе открыл админку — fail-open!")
|
||||
}
|
||||
if !authLockedOut() {
|
||||
t.Error("пропавший стор на настроенной системе должен блокировать админку")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user