Безопасность: 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:
+61
-8
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -56,17 +57,69 @@ func secureCookies(r *http.Request) bool {
|
||||
return httpsRequest(r) || !localRequest(r)
|
||||
}
|
||||
|
||||
// clientIP — адрес посетителя кабинета (за nginx — из X-Real-IP / X-Forwarded-For).
|
||||
// trustedProxies — сети, которым разрешено сообщать адрес клиента заголовками
|
||||
// X-Real-IP / X-Forwarded-For. По умолчанию — только loopback (nginx на том же
|
||||
// хосте). Расширяется списком CIDR/адресов в BJ_TRUSTED_PROXIES (через запятую).
|
||||
var trustedProxies = loadTrustedProxies(os.Getenv("BJ_TRUSTED_PROXIES"))
|
||||
|
||||
func loadTrustedProxies(s string) []*net.IPNet {
|
||||
var out []*net.IPNet
|
||||
for _, part := range strings.Split(s, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(part, "/") {
|
||||
if strings.Contains(part, ":") {
|
||||
part += "/128"
|
||||
} else {
|
||||
part += "/32"
|
||||
}
|
||||
}
|
||||
if _, n, err := net.ParseCIDR(part); err == nil {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// peerTrusted — непосредственный источник запроса (адрес соединения) — доверенный
|
||||
// прокси: loopback или явно указанный в BJ_TRUSTED_PROXIES.
|
||||
func peerTrusted(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if ip.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
for _, n := range trustedProxies {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// clientIP — адрес посетителя кабинета. Заголовки X-Real-IP / X-Forwarded-For
|
||||
// подставляет клиент, поэтому доверяем им ТОЛЬКО когда запрос пришёл от доверенного
|
||||
// прокси (по умолчанию nginx на том же хосте). Иначе адрес берём из соединения —
|
||||
// иначе злоумышленник подделал бы свой IP и обошёл лимит входа и журнал аудита.
|
||||
func clientIP(r *http.Request) string {
|
||||
if v := strings.TrimSpace(r.Header.Get("X-Real-IP")); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); v != "" {
|
||||
return strings.TrimSpace(strings.Split(v, ",")[0])
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
if peerTrusted(host) {
|
||||
if v := strings.TrimSpace(r.Header.Get("X-Real-IP")); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); v != "" {
|
||||
// самый правый элемент добавляет наш доверенный прокси (его peer);
|
||||
// левые может подставить клиент, поэтому берём правый.
|
||||
parts := strings.Split(v, ",")
|
||||
return strings.TrimSpace(parts[len(parts)-1])
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user