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:
marcusH
2026-07-19 23:47:29 +02:00
parent e245600235
commit 6d44f77671
6 changed files with 178 additions and 22 deletions
+37 -3
View File
@@ -39,10 +39,44 @@ describe("renderMarkdown", () => {
}
});
it("blockt data:-Bilder, lässt normale Ziele durch", () => {
it("blockt data:-Bilder", () => {
expect(renderMarkdown("![a](data:image/svg+xml,<svg onload=alert(1)>)")).not.toContain("<img");
expect(renderMarkdown("![a](https://example.org/b.png)")).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(`![alt](${src})`);
expect(el.querySelector("img"), `${src} durfte kein img erzeugen`).toBeNull();
expect(el.textContent).toContain("alt");
}
});
it("lässt lokale Bilder durch", () => {
expect(renderMarkdown("![a](./bild.png)")).toContain("<img");
expect(renderMarkdown("![a](/bild.png)")).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("&#39;");
});
it("escapt Anführungszeichen im title, damit das Attribut nicht aufbricht", () => {