Files
Bridge-and-Join-s/modules/lk/clientip_test.go
zuevav d975957335 Безопасность: 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
2026-09-18 21:25:35 +03:00

42 lines
2.1 KiB
Go

package lk
import (
"net/http"
"testing"
)
// H2: X-Forwarded-For/X-Real-IP от НЕдоверенного источника не должны подменять IP.
func TestClientIPTrust(t *testing.T) {
req := func(remote string, hdr map[string]string) *http.Request {
r, _ := http.NewRequest("POST", "/login", nil)
r.RemoteAddr = remote
for k, v := range hdr {
r.Header.Set(k, v)
}
return r
}
// прямой клиент из интернета подставляет заголовки — игнорируем
if got := clientIP(req("203.0.113.7:5000", map[string]string{"X-Real-IP": "1.2.3.4", "X-Forwarded-For": "9.9.9.9"})); got != "203.0.113.7" {
t.Errorf("недоверенный источник: ожидали 203.0.113.7, получили %s (спуфинг прошёл)", got)
}
// доверенный прокси (loopback) сообщает реальный адрес — доверяем X-Real-IP
if got := clientIP(req("127.0.0.1:5000", map[string]string{"X-Real-IP": "1.2.3.4"})); got != "1.2.3.4" {
t.Errorf("loopback X-Real-IP: ожидали 1.2.3.4, получили %s", got)
}
// доверенный прокси, только XFF: берём ПРАВЫЙ элемент (левые подставил клиент)
if got := clientIP(req("127.0.0.1:5000", map[string]string{"X-Forwarded-For": "9.9.9.9, 5.6.7.8"})); got != "5.6.7.8" {
t.Errorf("loopback XFF: ожидали правый 5.6.7.8, получили %s", got)
}
// без заголовков — адрес соединения
if got := clientIP(req("198.51.100.2:443", nil)); got != "198.51.100.2" {
t.Errorf("без заголовков: ожидали 198.51.100.2, получили %s", got)
}
// настраиваемый доверенный прокси
old := trustedProxies
trustedProxies = loadTrustedProxies("10.0.0.0/8")
defer func() { trustedProxies = old }()
if got := clientIP(req("10.1.2.3:5000", map[string]string{"X-Real-IP": "1.2.3.4"})); got != "1.2.3.4" {
t.Errorf("доверенный CIDR: ожидали 1.2.3.4, получили %s", got)
}
}