
Consul Template validierte, wohin ein Symlink während der Template-Auswertung zeigte, aber der spätere Abruf der Abhängigkeiten las den ursprünglichen Pfad. Wurde der Link zwischen diesen Vorgängen neu ausgerichtet, wurde aus einem Dateiverweis innerhalb der Sandbox eine Offenlegung von Dateien außerhalb der Sandbox.
Consul Template hat validiert, wohin ein Symlink während der Template-Auswertung zeigte, aber der spätere Abruf der Abhängigkeit las den ursprünglichen Pfad. Wenn der Link zwischen diesen Vorgängen umgebogen wurde, wurde aus einem Dateiverweis innerhalb der Sandbox eine Datei-Offenlegung außerhalb der Sandbox.
Ich habe dieses Problem bei der Überprüfung von HashiCorp Consul Template gefunden, mit einer ganz bestimmten Sicherheitsfrage im Hinterkopf:
Wenn sandbox_path einen Symlink während der Template-Auswertung validiert, bleibt der spätere Dateilesevorgang an dasselbe validierte Ziel gebunden?
In diesem Fall lautete die Antwort: nein.
Der file-Template-Helper löste den Pfad auf und erzwang die konfigurierte Sandbox während der Template-Auswertung. Aber nach dieser Prüfung erzeugte er eine Dateiabhängigkeit unter Verwendung des ursprünglichen Rohpfads.
Diese Abhängigkeit wurde später abgerufen.
Wenn ein Angreifer den Symlink in der Lücke zwischen Validierung und Abruf der Abhängigkeit umbog, konnte Consul Template eine Datei außerhalb der Sandbox lesen. Wenn der Link vor dem nächsten Rendern wiederhergestellt wurde, bestand die Sandbox-Validierung erneut und der zwischengespeicherte externe Inhalt wurde weiterhin gerendert.
Dieses Problem wurde CVE-2026-5061.
HashiCorp-Bulletin: HCSEC-2026-12
IBM-Bulletin: CVE-2026-5061 Security Bulletin
CVE: CVE-2026-5061
Behoben in: 0.42.0
photo0
vom Angreifer kontrollierter Symlink in der Sandbox -> Sandbox-Validierung löst zu sicherem Ziel auf -> Abhängigkeit speichert ursprünglichen Rohpfad -> Angreifer biegt Link außerhalb der Sandbox um -> Abruf der Abhängigkeit liest externe Datei -> Angreifer stellt sicheren Link wieder her -> nächste Validierung besteht -> zwischengespeicherter externer Inhalt wird gerendert
Consul Template ist ein Template-Rendering-Werkzeug für Consul- und Vault-Daten.
Es kann kontinuierlich laufen, Abhängigkeiten auf Änderungen überwachen und Daten in Dateien oder Umgebungsvariablen rendern, die von Anwendungen konsumiert werden können.
Der file-Template-Helper liest eine lokale Datei und fügt ihren Inhalt in die gerenderte Ausgabe ein.
Da lokale Dateilesevorgänge prozesslesbare Geheimnisse offenlegen können, stellt Consul Template sandbox_path als Grenze für diesen Helper bereit.
Die dokumentierte Sicherheitseigenschaft ist unkompliziert:
file übergebene Pfade müssen innerhalb der konfigurierten Sandbox liegenDas macht sandbox_path zu einer echten Sicherheitsgrenze.
Die wichtige Frage war nicht, ob der Pfad so aussah, als läge er in der Sandbox.
Die eigentliche Frage war:
Bleibt die Datei, die gelesen wird, dieselbe Datei, die die Sandbox-Validierung bestanden hat?
In verwundbaren Versionen war das nicht der Fall.
Dateisystem-Sandboxes scheitern oft an der Lücke zwischen Pfadvalidierung und Dateizugriff.
Das übliche Muster ist:
Diese Annahme ist unsicher, wenn ein Angreifer einen Symlink oder eine gleichwertige Dateisystem-Umleitung zwischen den beiden Vorgängen verändern kann.
Consul Template machte diese Angriffsfläche besonders interessant, weil Template-Auswertung und Abruf der Abhängigkeiten getrennte Phasen waren.
Diese Trennung erzeugte die richtige Sicherheitsfrage:
Ist der Abruf der Abhängigkeit an das validierte Ziel gebunden, oder löst er den vom Angreifer kontrollierten Pfad erneut auf?
Das war die Grenze, auf die ich mich konzentriert habe.
Die Grundursache war eine Time-of-Check-to-Time-of-Use-Diskrepanz zwischen:
template/funcs.godependency/file.goIn der getesteten Revision tat fileFunc() Folgendes:
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(s)
Der Validierungshelfer löste Symlinks vor der Containment-Prüfung korrekt auf:
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))
rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("'%s' is outside of sandbox", path)
}
Die Sandbox-Prüfung selbst verstand also das aufgelöste Ziel.
Aber dieses aufgelöste Ziel wurde verworfen.
NewFileQuery() speicherte stattdessen den ursprünglichen String:
return &FileQuery{
stopCh: make(chan struct{}, 1),
path: s,
}, nil
Der spätere Abruf der Abhängigkeit las diesen Pfad dann erneut:
data, err := os.ReadFile(d.path)
Das ist die gesamte Verwundbarkeit.
Der Code prüfte eine Dateisystemauflösung und verwendete später eine andere.
Weil ein Symlink veränderbar ist.
Der Angreifer muss kein Ziel außerhalb der Sandbox dazu bringen, pathInSandbox() direkt zu bestehen.
Er muss nur ändern, wohin der bereits genehmigte Rohpfad nach der Validierung, aber vor dem Fetch()-Lesevorgang auflöst.
Die Abfolge ist:
pathInSandbox() zu einer sicheren Datei aufFileQuery zeichnet den Rohpfad des Links aufos.ReadFile(d.path) löst den Link erneut aufDieser letzte Schritt ist wichtig.
Das Wiederherstellen des sicheren Links entfernte das bereits abgerufene Geheimnis nicht aus dem Abhängigkeits-Cache.
Der wichtige Unterschied ist die Umgehung einer expliziten Sicherheitskontrollmaßnahme.
Dies war nicht nur:
„die Datei hat sich geändert, während Consul Template sie beobachtet hat“
Das Beobachten von Dateien auf Änderungen ist erwartetes Verhalten.
Das eigentliche Problem war:
ein Pfad, der die dokumentierte Sandbox-Beschränkung bestanden hatte, konnte später verwendet werden, um eine Datei außerhalb dieser Sandbox zu lesen
Das ist ein direkter Vertrauensgrenzen-Fehler.
Die Anwendung hatte bereits eine Sicherheitsentscheidung getroffen:
Aber der spätere Abruf war nicht an das Ziel gebunden, das diese Entscheidung rechtfertigte.
Das ist es, was gewöhnliche Dateisystem-Veränderlichkeit in eine Verwundbarkeit verwandelte.
Ich habe einen eigenständigen Reproducer um die exakte Auswertungs- und Abhängigkeitsabruf-Sequenz herum gebaut.
Der kontrollierte Aufbau verwendete: