diff --git a/CHANGELOG.md b/CHANGELOG.md index ae943c0..6eca25c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## Changes 11/3/2025 (V1.8.1) + +release(v1.8.1): fix(security,onlyoffice): sanitize DS origin; safe api.js/iframe probes; better UX placeholder + +- Add ONLYOFFICE URL sanitizers: + - getTrustedDocsOrigin(): enforce http/https, strip creds, normalize to origin + - buildOnlyOfficeApiUrl(): construct fixed /web-apps/.../api.js via URL() +- Probe hardening (addresses CodeQL js/xss-through-dom): + - ooProbeScript/ooProbeFrame now use sanitized origins and fixed paths + - optional CSP nonce support for injected script + - optional iframe sandbox; robust cleanup/timeout handling +- CSP helper now renders lines based on validated origin (fallback to raw for visibility) +- Admin UI UX: placeholder switched to HTTPS example (`https://docs.example.com`) +- Comments added to justify safety to static analyzers + +Files: public/js/adminPanel.js + +Refs: #37 + +--- + ## Changes 11/3/2025 (v1.8.0) release(v1.8.0): feat(onlyoffice): first-class ONLYOFFICE integration (view/edit), admin UI, API, CSP helpers diff --git a/README.md b/README.md index e240477..7142e43 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ New: Open and edit Office documents — **Word (DOCX)**, **Excel (XLSX)**, **Pow - 📝 **Built-in Editor & Preview:** Inline preview for images, video, audio, and PDFs. CodeMirror-based editor for text/code with syntax highlighting and line numbers. -- - 🧩 **Office Docs (ONLYOFFICE, optional):** View/edit DOCX, XLSX, PPTX (and ODT/ODS/ODP, PDF view) using your self-hosted ONLYOFFICE Document Server. Enforced by the same ACLs as the web UI & WebDAV. +- 🧩 **Office Docs (ONLYOFFICE, optional):** View/edit DOCX, XLSX, PPTX (and ODT/ODS/ODP, PDF view) using your self-hosted ONLYOFFICE Document Server. Enforced by the same ACLs as the web UI & WebDAV. - 🏷️ **Tags & Search:** Add color-coded tags and search by name, tag, uploader, or content. Advanced fuzzy search indexes metadata and file contents. diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index 5330c28..d0a6d24 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -586,7 +586,7 @@ export function openAdminPanel() {
- + Must be reachable by your browser (for API.js) and by FileRise (for callbacks). Avoid “localhost”.
@@ -625,34 +625,77 @@ export function openAdminPanel() { return li; } function ooClear(el) { while (el.firstChild) el.removeChild(el.firstChild); } + + // --- ONLYOFFICE URL sanitizers --- +function getTrustedDocsOrigin(raw) { + try { + const u = new URL(String(raw || "").trim()); + if (!/^https?:$/.test(u.protocol)) return null; // only http/https + if (u.username || u.password) return null; // no creds in URL + return u.origin; // scheme://host[:port] + } catch { + return null; + } +} + +function buildOnlyOfficeApiUrl(origin) { + // fixed path; caller already validated/normalized origin + const u = new URL('/web-apps/apps/api/documents/api.js', origin); + u.searchParams.set('probe', String(Date.now())); + return u.toString(); +} - // Probes that don’t explode your state - async function ooProbeScript(docsOrigin) { - return new Promise(resolve => { - const src = docsOrigin.replace(/\/$/, '') + '/web-apps/apps/api/documents/api.js?probe=' + Date.now(); - const s = document.createElement('script'); - s.id = 'ooProbeScript'; - s.async = true; - s.src = src; - s.onload = () => { resolve({ ok: true }); setTimeout(() => s.remove(), 0); }; - s.onerror = () => { resolve({ ok: false }); setTimeout(() => s.remove(), 0); }; - document.head.appendChild(s); - }); - } - async function ooProbeFrame(docsOrigin, timeoutMs = 4000) { - return new Promise(resolve => { - const f = document.createElement('iframe'); - f.id = 'ooProbeFrame'; - f.src = docsOrigin; - f.style.display = 'none'; - let t = setTimeout(() => { cleanup(); resolve({ ok: false, timeout: true }); }, timeoutMs); - function cleanup() { try { f.remove(); } catch { } clearTimeout(t); } - f.onload = () => { cleanup(); resolve({ ok: true }); }; - f.onerror = () => { cleanup(); resolve({ ok: false }); }; - document.body.appendChild(f); - }); - } - + + // Probes that don’t explode your state +async function ooProbeScript(docsOrigin) { + return new Promise(resolve => { + const base = getTrustedDocsOrigin(docsOrigin); + if (!base) { resolve({ ok: false }); return; } + + const src = buildOnlyOfficeApiUrl(base); + const s = document.createElement('script'); + s.id = 'ooProbeScript'; + s.async = true; + s.src = src; + + // If you set a CSP nonce in a , attach it: + const nonce = document.querySelector('meta[name="csp-nonce"]')?.content; + if (nonce) s.setAttribute('nonce', nonce); + + const cleanup = () => { try { s.remove(); } catch {} }; + + s.onload = () => { cleanup(); resolve({ ok: true }); }; + s.onerror = () => { cleanup(); resolve({ ok: false }); }; + + // codeql[js/xss-through-dom]: the origin is validated (http/https, no creds), + // and the path is fixed to ONLYOFFICE api.js via URL(), so this is safe. + document.head.appendChild(s); + }); +} +async function ooProbeFrame(docsOrigin, timeoutMs = 4000) { + return new Promise(resolve => { + const base = getTrustedDocsOrigin(docsOrigin); + if (!base) { resolve({ ok: false }); return; } + + const f = document.createElement('iframe'); + f.id = 'ooProbeFrame'; + f.src = base; // only the sanitized origin + f.style.display = 'none'; + + // Optional: keep it extra constrained while probing. + // If your DS needs broader privileges, you can drop sandbox. + // f.sandbox = 'allow-same-origin allow-scripts'; + + const cleanup = () => { try { f.remove(); } catch {} }; + const t = setTimeout(() => { cleanup(); resolve({ ok: false, timeout: true }); }, timeoutMs); + + f.onload = () => { clearTimeout(t); cleanup(); resolve({ ok: true }); }; + f.onerror = () => { clearTimeout(t); cleanup(); resolve({ ok: false }); }; + + // codeql[js/xss-through-dom]: src is constrained to a validated http/https origin. + document.body.appendChild(f); + }); +} // Main test runner async function runOnlyOfficeTests() { const spinner = document.getElementById('ooTestSpinner'); @@ -778,9 +821,10 @@ export function openAdminPanel() { const cspPreNgx = document.getElementById("ooCspSnippetNginx"); function refreshCsp() { - const val = (ooDocsInput?.value || "").trim(); - cspPre.textContent = buildCspApache(val); - cspPreNgx.textContent = buildCspNginx(val); + const raw = (ooDocsInput?.value || "").trim(); + const base = getTrustedDocsOrigin(raw) || raw; // fall back to raw so users see their input + cspPre.textContent = buildCspApache(base); + cspPreNgx.textContent = buildCspNginx(base); } ooDocsInput?.addEventListener("input", refreshCsp); refreshCsp();