Alle Behebungen auf der Linux-Test-VM gegengeprueft (Ubuntu, .NET 10.0.111): Build 0 Warnungen / 0 Fehler, 637 Tests gruen, 1 uebersprungen (WindowsFact). Sicherheit und Zielplattform #373 PathBoundary.Canonicalize fiel offen aus. File.ResolveLinkTarget wirft fuer einen fehlenden Abschnitt FileNotFoundException; der catch(IOException) verliess daraufhin die GANZE Aufloesung mit dem unaufgeloesten Pfad - und der besteht jede Praefixpruefung. Der Symlink-Schutz war damit bei jedem Schreibvorgang wirkungslos. Jetzt ueber FileSystemInfo.LinkTarget, und TryCanonicalize meldet "nicht bestimmbar", statt fail-open durchzureichen. #374 WorkspacePath weist den Backslash rundheraus ab, wie den Doppelpunkt. Eine Schreibweise statt einer Sonderregel je Windows-Form. #375 Log-Verzeichnis: leer heisst "Anwendungseinstellung"; ein relativer Wert zaehlt gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses. Ein systemd-Dienst landet nicht mehr auf /Logs. Agentenkommunikation #206 Delegationskette gegen A->B->A (Test A13). Zyklusfrei per Bauart, dazu MaxDelegationDepth gegen die Staffel, die niemand beauftragt hat. #461 Zwei unabhaengige Ketten ueber Kreuz - beim Beheben von #206 gefunden und reproduziert. Ein delegierter Lauf wartet nur noch begrenzt auf ein fremdes Gate; ein Chat vom Menschen reiht sich weiter unbegrenzt ein. #234 Instanz-Id wird durchgereicht statt aus einem nur beilaeufig gesetzten Feld gelesen. Betrieb und Oberflaeche #376 Log-Ansicht chronologisch statt gruppiert je Moduldatei, mit Stacktraces an ihrer Meldung - auch ueber Taktgrenzen hinweg. #377 Info-Tabelle: feste Spaltenbreiten, damit ein langer Aenderungstext den anderen Spalten nicht den Platz nimmt. #378 Sicherung: Mindestabstand zwischen Ueberschrift und Schaltflaechenleiste. #379 Hellmodus: drei von vier gedaempften Textpinseln lagen unter 4,5:1. Die helle Palette ist als Ganzes verschoben, damit die Staffelung sichtbar bleibt statt auf einen Wert zusammenzufallen. Hygiene #233 stock_add: der Indexstand wird innerhalb der Sperre festgehalten. #235 Zeitumstellung: eine nicht existierende Ortszeit wird auf den ersten Zeitpunkt nach der Luecke verschoben statt uebersprungen (R4, R5). #236 45 Nullable-Warnungen und eine CA1416 - nicht unterdrueckt, sondern die Stellen so umgebaut, dass die Zusage nachvollziehbar wird. Mail meldet jetzt, WELCHER Zugangsschluessel fehlt, statt an einer KeyNotFoundException zu scheitern. #203 Kompaktiert wird vor dem API-Aufruf statt danach; die gemessenen Prompt-Groessen kalibrieren die Schaetzung je Modell. Neu: tests/ClawdDotNet.App.Tests - fuer #375 und #376 gab es keinen Ort fuer Tests. Dazu Regressionstests fuer jede Behebung; jeder wurde gegen den alten Stand geprueft, damit belegt ist, dass er den Fehler faengt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
6.4 KiB
C#
164 lines
6.4 KiB
C#
using ClawdDotNet.Tools.FileRW;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Tools.Tests.FileRW;
|
|
|
|
/// <summary>
|
|
/// S6 aus der Bestandsaufnahme: Die Pfadprüfung verglich nur Zeichenketten-Präfixe.
|
|
/// Ohne abschließenden Verzeichnistrenner erlaubte ein Root "…\Workspace" damit auch
|
|
/// "…\Workspace-Backup\…".
|
|
///
|
|
/// Das Dateisystem wird hier bewusst NICHT abstrahiert — die Tests sollen die echte
|
|
/// Pfadsemantik der jeweiligen Plattform prüfen (.., UNC, Alternate Data Streams,
|
|
/// abschließende Punkte, unter Linux zusätzlich Groß-/Kleinschreibung und
|
|
/// Verknüpfungen). Eine Abstraktion würde genau die Fehlerklasse verstecken, um die es
|
|
/// geht.
|
|
///
|
|
/// Die Verzeichnisgrenze selbst prüft <c>PathBoundaryTests</c> in Core.Tests; hier geht
|
|
/// es um das, was <see cref="WorkspacePath.Resolve"/> darüber hinaus abweist.
|
|
/// </summary>
|
|
public sealed class WorkspacePathTests : IDisposable
|
|
{
|
|
private readonly string _root;
|
|
|
|
public WorkspacePathTests()
|
|
{
|
|
_root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace");
|
|
Directory.CreateDirectory(_root);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(Path.GetDirectoryName(_root)!, recursive: true); }
|
|
catch { /* Aufräumen ist Nebensache */ }
|
|
}
|
|
|
|
private string Resolve(string? relative) => WorkspacePath.Resolve(_root, relative, "personal");
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Ausbruchsversuche
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Theory]
|
|
[InlineData("../../../Windows/System32/drivers/etc/hosts")]
|
|
[InlineData(@"..\..\evil.txt")]
|
|
[InlineData("unterordner/../../../ausserhalb.txt")]
|
|
[InlineData("./../../evil.txt")]
|
|
[InlineData("..")]
|
|
public void Relative_Ausbrueche_werden_abgelehnt(string path)
|
|
{
|
|
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Die Windows-Formen müssen auch unter Linux abgewiesen werden — dort greift
|
|
/// aber eine andere Regel: <c>Path.IsPathRooted(@"C:\temp\x")</c> ist unter Linux
|
|
/// <c>false</c>, weil das schlicht ein Dateiname mit Doppelpunkt ist. Gefangen
|
|
/// werden diese Fälle von der Zeichenregel: Doppelpunkt und Backslash sind im
|
|
/// Pfad unzulässig, getrennt wird mit <c>/</c>. Das Ergebnis ist auf beiden
|
|
/// Plattformen dasselbe, der Weg dorthin nicht.
|
|
///
|
|
/// Dieselbe Regel fängt unter Linux auch <c>@"..\..\evil.txt"</c> aus dem Test
|
|
/// darüber. Ein Ausbruch war das dort nie — der Backslash trennt unter Linux
|
|
/// nichts, die Datei wäre im Workspace gelandet —, aber der Test erwartete eine
|
|
/// Abweisung und bekam keine.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(@"C:\Windows\System32\config\SAM")]
|
|
[InlineData(@"\\server\share\evil.txt")]
|
|
[InlineData("//server/share/evil.txt")]
|
|
[InlineData(@"C:\temp\datei.txt")]
|
|
public void Absolute_Pfade_und_UNC_Freigaben_werden_abgelehnt(string path)
|
|
{
|
|
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Die Linux-Entsprechungen. Unter Windows sind das gewöhnliche relative Pfade,
|
|
/// die entweder ins Leere zeigen oder von der <c>..</c>-Regel gefangen werden —
|
|
/// deshalb laufen sie dort mit, statt übersprungen zu werden.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("/etc/passwd")]
|
|
[InlineData("/root/.ssh/id_rsa")]
|
|
[InlineData("../../../../etc/shadow")]
|
|
public void Unix_Systempfade_werden_abgelehnt(string path)
|
|
{
|
|
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("datei.txt:versteckt")]
|
|
[InlineData("datei.txt:$DATA")]
|
|
public void Alternate_Data_Streams_werden_abgelehnt(string path)
|
|
{
|
|
// Ein ADS umgeht sonst die Endungsprüfung: "x.txt:evil.exe".
|
|
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Der eigentliche Kern von S6: ein Nachbarverzeichnis mit gleichem Präfix.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_gilt_als_ausserhalb()
|
|
{
|
|
var backup = _root + "-Backup";
|
|
|
|
WorkspacePath.IsInside(Path.Combine(backup, "geheim.txt"), _root).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Auch_bei_Rootangabe_mit_Trenner_bleibt_das_Nachbarverzeichnis_aussen()
|
|
{
|
|
var rootWithSeparator = _root + Path.DirectorySeparatorChar;
|
|
|
|
WorkspacePath.IsInside(_root + "-Backup" + Path.DirectorySeparatorChar + "x.txt", rootWithSeparator)
|
|
.ShouldBeFalse();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Gegenproben — normale Nutzung muss funktionieren
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Theory]
|
|
[InlineData("bericht.md")]
|
|
[InlineData("unterordner/bericht.md")]
|
|
[InlineData("a/b/c/tief.json")]
|
|
[InlineData("./bericht.md")]
|
|
[InlineData("unterordner/../bericht.md")]
|
|
public void Pfade_innerhalb_des_Workspace_werden_aufgeloest(string path)
|
|
{
|
|
var resolved = Resolve(path);
|
|
|
|
WorkspacePath.IsInside(resolved, _root).ShouldBeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData(".")]
|
|
public void Leere_Angaben_ergeben_das_Wurzelverzeichnis(string? path)
|
|
{
|
|
var resolved = Resolve(path);
|
|
|
|
resolved.TrimEnd(Path.DirectorySeparatorChar)
|
|
.ShouldBe(Path.GetFullPath(_root).TrimEnd(Path.DirectorySeparatorChar));
|
|
}
|
|
|
|
[Fact]
|
|
public void Das_Wurzelverzeichnis_selbst_gilt_als_innerhalb()
|
|
{
|
|
WorkspacePath.IsInside(_root, _root).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Ein_Unterverzeichnis_mit_aehnlichem_Namen_bleibt_innerhalb()
|
|
{
|
|
// Gegenprobe zur Präfix-Regel: Innerhalb des Roots ist alles erlaubt.
|
|
var inner = Path.Combine(_root, "Workspace-Backup", "x.txt");
|
|
|
|
WorkspacePath.IsInside(inner, _root).ShouldBeTrue();
|
|
}
|
|
}
|