Files
Loki/internal/loki/web_auth_crosssite_test.go
T
MichaelandClaude Opus 5.5 b893d1a81f API : un site tiers ne peut plus piloter Loki en douce
Repris d'AJEAN 0.15.5. Sans clé de pilotage (le défaut), l'API /api était
ouverte à tout ce qui savait joindre le port — y compris une page web
quelconque ouverte dans un navigateur du réseau local. Un POST « simple »
(text/plain) ne déclenche aucune pré-vérification CORS : la page pouvait
changer des réglages et, mode agent actif, faire exécuter des commandes.

requireWebAuth passe désormais par crossSiteReject, AVANT le test de clé :
- Sec-Fetch-Site: cross-site → 403 ;
- Origin présent et différent de l'hôte appelé (ou « null ») → 403. curl,
  les scripts et les apps n'envoient pas d'Origin : non concernés ;
- sans clé seulement, l'hôte appelé doit être local (IP, localhost, nom sans
  point, .local/.lan/.home…, nom du conteneur) : c'est ce qui coupe le DNS
  rebinding, où un domaine malveillant se fait résoudre en IP locale.

DEUX ÉCARTS AVEC L'AMONT, DUS AU CONTENEUR
- LOKI_TRUSTED_HOSTS : derrière un reverse proxy, le nom public n'est ni
  local ni celui du conteneur. Plutôt qu'imposer une clé, on peut lister ce
  nom. Documenté dans le README.
- Le trafic du tunnel (marqué par withLocalAuth, authentifié par le relais)
  est dispensé du contrôle d'hôte : son Host est celui du relais.

À SAVOIR : un accès existant par nom de domaine SANS clé ni
LOKI_TRUSTED_HOSTS est désormais refusé (403, message explicite).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:04:05 +02:00

47 lines
1.6 KiB
Go

package loki
import (
"net/http/httptest"
"testing"
)
func TestCrossSiteReject(t *testing.T) {
t.Setenv("LOKI_TRUSTED_HOSTS", "loki.exemple.fr")
cas := []struct {
nom string
host, origin string
fetchSite string
tunnel bool
keyed bool
refuse bool
}{
{nom: "curl sur IP", host: "192.168.1.10:8090"},
{nom: "localhost", host: "localhost:8090", origin: "http://localhost:8090"},
{nom: "nom sans point", host: "tower:8090", origin: "http://tower:8090"},
{nom: "nom .local", host: "tower.local:8090"},
{nom: "origine tierce", host: "192.168.1.10:8090", origin: "http://evil.example", refuse: true},
{nom: "origine null", host: "localhost:8090", origin: "null", refuse: true},
{nom: "sec-fetch cross-site", host: "localhost:8090", fetchSite: "cross-site", refuse: true},
{nom: "rebinding sans clé", host: "evil.example:8090", origin: "http://evil.example:8090", refuse: true},
{nom: "domaine avec clé", host: "evil.example:8090", keyed: true},
{nom: "domaine de confiance", host: "loki.exemple.fr", origin: "https://loki.exemple.fr"},
{nom: "tunnel", host: "relais.exemple.net", tunnel: true},
}
for _, c := range cas {
r := httptest.NewRequest("POST", "/api/status", nil)
r.Host = c.host
if c.origin != "" {
r.Header.Set("Origin", c.origin)
}
if c.fetchSite != "" {
r.Header.Set("Sec-Fetch-Site", c.fetchSite)
}
if c.tunnel {
r.Header.Set(viaTunnelHeader, "tunnel")
}
if got := crossSiteReject(r, c.keyed) != ""; got != c.refuse {
t.Errorf("%s : refus=%v, attendu %v", c.nom, got, c.refuse)
}
}
}