Sicherheitscheck: Schwachstellen behoben

- Pfad-Traversal: safeFilename/containedPath-Helper, storeAnhang sanitizes
  filename, alle Download/Delete/Mail-Send-Routen pruefen Containment
- Stored XSS: serveInline entscheidet Viewable-Typ nur nach Extension,
  nicht nach client/seitigem MIME; nicht viewbare Typen werden als Download
  erzwungen. Upload fileFilter (Basis/Interne) + Extension-Validierung
  (Signatur/Foto leiten Ext aus MIME, blockieren .html)
- URL-Scheme-Allowlist (safeUrl) fuer quelle_url-hrefs gegen javascript:-XSS
- E-Mail-Iframe: Sandbox auf allow-same-only (kein allow-popups-to-escape)
- Sicherheits-Header: CSP, X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, COOP; x-powered-by aus; jsPDF self-hosted unter /vendor
- Session: Secure-Flag bei TLS, serverseitige absoluteexpiry, Scrypt async
  + Dummy-Verify gegen Timing/Enumerate + Login-Rate-Limit
- Open Redirect: /email/fetch nur same-origin Redirects
- SSRF: Validierung von OLLAMA_HOST/CALDAV_URL/MAIL_HOST gegen
  Metadata/Link-Local-BLock (localhost/LAN bleibt erlaubt)
- Globaler Error-Handler ohne Interna-Leak, env=production

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 02:09:17 +02:00
co-authored by Claude
parent da0b497972
commit fd1db3970f
10 changed files with 751 additions and 76 deletions
+5 -4
View File
@@ -415,7 +415,7 @@
</summary>
<div class="px-4 pb-4">
<% if (application.quelle_url) { %>
<a href="<%= application.quelle_url %>" target="_blank" rel="noopener"
<a href="<%= safeUrl(application.quelle_url) %>" target="_blank" rel="noopener"
class="inline-block mb-3 text-sm text-blue-600 dark:text-blue-400 hover:underline break-all"><%= application.quelle_url %></a>
<% } %>
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= application.stellenbeschreibung %></p>
@@ -471,7 +471,7 @@
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1"><%= e.subject || '(kein Betreff)' %></p>
<% if (e.display_srcdoc) { %>
<iframe class="email-html-frame w-full border border-gray-200 dark:border-gray-600 rounded bg-white" style="height:6rem"
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer"
sandbox="allow-same-origin" referrerpolicy="no-referrer"
srcdoc="<%= e.display_srcdoc %>"></iframe>
<% } else { %>
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= e.body_text || '' %></p>
@@ -828,8 +828,9 @@
});
// Size a sandboxed e-mail iframe to its content height. The sandbox
// enables allow-same-origin (but not allow-scripts), so scripts in the
// mail never run, yet we can still measure the rendered document.
// keeps allow-same-origin (so we can read contentDocument to measure)
// but enables no scripts, no forms and no popups, so mail content can
// neither execute nor escape the frame.
function fitFrame(f) {
try {
const doc = f.contentDocument || (f.contentWindow && f.contentWindow.document);
+1 -1
View File
@@ -116,7 +116,7 @@
<% } %>
<% } %>
<% if (j.quelle_url) { %>
<a href="<%= j.quelle_url %>" target="_blank" rel="noopener noreferrer"
<a href="<%= safeUrl(j.quelle_url) %>" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline mt-2 break-all">
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
<span class="truncate"><%= j.quelle_url %></span>
+1 -1
View File
@@ -77,7 +77,7 @@
</p>
<% } %>
<% if (j.quelle_url) { %>
<a href="<%= j.quelle_url %>" target="_blank" rel="noopener noreferrer"
<a href="<%= safeUrl(j.quelle_url) %>" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline mt-2 break-all">
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
<span class="truncate"><%= j.quelle_url %></span>
+1 -1
View File
@@ -62,7 +62,7 @@
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1"><%= e.subject || '(kein Betreff)' %></p>
<% if (e.display_srcdoc) { %>
<iframe class="email-html-frame w-full border border-gray-200 dark:border-gray-600 rounded bg-white" style="height:6rem"
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer"
sandbox="allow-same-origin" referrerpolicy="no-referrer"
srcdoc="<%= e.display_srcdoc %>"></iframe>
<% } else { %>
<p class="email-body whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300 max-h-40 overflow-hidden"><%= e.body_text || '' %></p>