Sicherheitsrunde: Befehlsinjektion, Pool-Pfad, Namensprüfung, Bild-Beacon
Vier Befunde aus einer Prüfung entlang der Angriffsflächen (MCP-Tools, Command-Grenze, Credentials, Schreibpfade und Abhängigkeiten): - todo.rs: Der SessionStart-Hook interpolierte den Projektpfad unquotiert in eine Shell-Kommandozeile, die Claude Code bei jedem Sessionstart ausfuehrt. Ein Ordner `repo$(...)` -- etwa aus einem geklonten Fremd-Repo -- ergab dauerhafte Codeausfuehrung; ein Leerzeichen zerbrach den Hook still. Der Pfad wird jetzt gequotet, inklusive Apostroph. - project_pool_dir: Der Pool-Name kommt aus der ai-control.json im Projektordner, also aus einer versionierten Datei. Ungeprueft bestimmte er mit ../ ein beliebiges Verzeichnis als CLAUDE_CONFIG_DIR, dessen settings.json wiederum apiKeyHelper traegt -- ein Kommando, das beim Sessionstart laeuft. Jetzt durch check_name abgesichert. - check_name: Steuerzeichen, Backslash und fuehrender Punkt werden abgelehnt. Ein Newline im Projektnamen hing bisher eigene Schluessel an die .desktop- Datei; Exec= braucht keinen Schraegstrich, den der alte Check verbot. - markdown.ts: Bilder duerfen nur noch lokale Ziele laden. Ein auswaertiges <img> laedt ohne Zutun und meldet damit IP, Zeitpunkt und im Pfad kodierte Daten -- der Panelinhalt stammt aus einer LLM-Session, ist also injizierbar. Schema-relative URLs (//host/x) waren durch ein zu laxes Muster erfasst. Links bleiben auswaerts erlaubt, jetzt mit rel="noopener noreferrer". escapeHtml escaped zusaetzlich das Apostroph. Dazu cargo audit nachgeholt: plist 1.9.0 -> 1.10.0 hebt quick-xml auf 0.41.0 und schliesst RUSTSEC-2026-0194/0195 fuer den Laufzeitpfad. Die zweite Instanz von quick-xml 0.39.4 bleibt ueber wayland-scanner im Lockfile -- ein proc-macro, das nur zur Compile-Zeit laeuft und ausgelieferte Protokoll-XMLs parst, nicht im Binary landet. 77 Rust-Tests, 39 Frontend-Tests.
This commit is contained in:
+37
-3
@@ -39,10 +39,44 @@ describe("renderMarkdown", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("blockt data:-Bilder, lässt normale Ziele durch", () => {
|
||||
it("blockt data:-Bilder", () => {
|
||||
expect(renderMarkdown(">)")).not.toContain("<img");
|
||||
expect(renderMarkdown("")).toContain("<img");
|
||||
expect(renderMarkdown("[a](https://example.org)")).toContain('href="https://example.org"');
|
||||
});
|
||||
|
||||
/// Ein <img> lädt ohne Zutun. Auswärtige Bildquellen sind darum ein
|
||||
/// Zero-Click-Beacon: IP, Zeitpunkt und im Pfad kodierte Daten gehen raus,
|
||||
/// sobald jemand ein vergiftetes Archiv-Dokument nur ansieht.
|
||||
it("lädt keine auswärtigen Bilder, auch nicht schema-relativ", () => {
|
||||
for (const src of [
|
||||
"https://evil.example/t.png",
|
||||
"http://evil.example/t.png",
|
||||
"//evil.example/t.png",
|
||||
"HTTPS://evil.example/t.png",
|
||||
]) {
|
||||
const el = document.createElement("div");
|
||||
el.innerHTML = renderMarkdown(``);
|
||||
expect(el.querySelector("img"), `${src} durfte kein img erzeugen`).toBeNull();
|
||||
expect(el.textContent).toContain("alt");
|
||||
}
|
||||
});
|
||||
|
||||
it("lässt lokale Bilder durch", () => {
|
||||
expect(renderMarkdown("")).toContain("<img");
|
||||
expect(renderMarkdown("")).toContain("<img");
|
||||
});
|
||||
|
||||
/// Links dürfen auswärts zeigen — sie brauchen einen Klick.
|
||||
it("erlaubt auswärtige Links, aber ohne Referrer und Opener", () => {
|
||||
const el = document.createElement("div");
|
||||
el.innerHTML = renderMarkdown("[a](https://example.org)");
|
||||
const a = el.querySelector("a")!;
|
||||
expect(a.getAttribute("href")).toBe("https://example.org");
|
||||
expect(a.getAttribute("rel")).toContain("noopener");
|
||||
expect(a.getAttribute("rel")).toContain("noreferrer");
|
||||
});
|
||||
|
||||
it("escapt auch das Apostroph", () => {
|
||||
expect(renderMarkdown("Text mit ' Apostroph")).toContain("'");
|
||||
});
|
||||
|
||||
it("escapt Anführungszeichen im title, damit das Attribut nicht aufbricht", () => {
|
||||
|
||||
Reference in New Issue
Block a user