d975957335
По результатам 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
42 lines
2.1 KiB
Go
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)
|
|
}
|
|
}
|