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();