using ClawdDotNet.Core.Storage; namespace ClawdDotNet.Tools.FileRW; /// /// Löst relative Pfade innerhalb eines Workspace auf und stellt sicher, dass sie /// diesen nicht verlassen. /// /// Hintergrund (S6): Die Prüfung verglich vorher nur die Zeichenkette: /// /// fullPath.StartsWith(rootPath, OrdinalIgnoreCase) /// /// Ohne abschließenden Verzeichnistrenner erlaubte ein Root wie /// "…\Agent-X\Workspace" damit auch Zugriffe auf "…\Agent-X\Workspace-Backup\…" — /// ein fremdes Verzeichnis, das zufällig mit demselben Präfix beginnt. /// /// Der Vergleich selbst liegt in : Unter Linux /// unterscheidet das Dateisystem Groß- und Kleinschreibung, und symbolische /// Verknüpfungen können aus dem Workspace hinauszeigen, ohne dass die reine /// Pfadrechnung das sieht. /// public static class WorkspacePath { /// /// Setzt auf auf und prüft, /// dass das Ergebnis innerhalb des Workspace liegt. /// /// Wenn der Pfad ausbricht. public static string Resolve(string root, string? relativePath, string workspaceName) { var value = string.IsNullOrWhiteSpace(relativePath) ? "." : relativePath.Trim(); // Absolute Pfade und UNC-Freigaben würden Path.Combine den Root verwerfen lassen. if (Path.IsPathRooted(value) || value.StartsWith("//")) { throw new UnauthorizedAccessException( $"Access denied: Absolute paths are not allowed in the {workspaceName} workspace."); } // Doppelpunkt und Backslash werden rundheraus abgewiesen. // // Beide sind unter Linux gewöhnliche Namenszeichen, und genau daran scheiterte // die Abwehr der Windows-Formen: Path.IsPathRooted(@"C:\temp\x") meldet dort // false, und @"..\..\evil.txt" ist ein einzelner Dateiname statt eines // Ausbruchs. Ein Ausbruch war das nie — die Datei blieb im Workspace —, aber // eine Sonderregel je Windows-Form (Doppelpunkt für Laufwerke, "\\" für UNC) // lässt immer eine Form übrig, die durchfällt. Hier gilt darum eine // Schreibweise: "/" trennt Abschnitte, alles andere ist ein Name. Beide Zeichen // sind unter Linux in Dateinamen zwar erlaubt, aber nichts, was ein Agent // braucht; die Einschränkung kostet uns nichts. // // Der Doppelpunkt fängt nebenbei Alternate Data Streams (datei.txt:versteckt), // die unter Windows die Endungsprüfung umgehen. if (value.Contains(':') || value.Contains('\\')) { throw new UnauthorizedAccessException( @"Access denied: Path contains an invalid character (':' or '\'). " + "Sections are separated with '/'."); } var normalizedRoot = NormalizeDirectory(root); var fullPath = Path.GetFullPath(Path.Combine(normalizedRoot, value)); if (!IsInside(fullPath, normalizedRoot)) { throw new UnauthorizedAccessException( $"Access denied: Path is outside the {workspaceName} workspace."); } return fullPath; } /// /// Prüft, ob im Verzeichnis liegt. /// Der Vergleich erfolgt auf Verzeichnisgrenzen, nicht auf Zeichenketten-Präfixen, /// und löst symbolische Verknüpfungen auf. /// public static bool IsInside(string candidate, string root) => PathBoundary.IsInside(candidate, root); /// Absoluter Pfad mit genau einem abschließenden Trenner. private static string NormalizeDirectory(string path) => PathBoundary.NormalizeDirectory(path); }