release(v1.8.1): fix(security,onlyoffice): sanitize DS origin; safe api.js/iframe probes; better UX placeholder

This commit is contained in:
Ryan
2025-11-03 16:59:47 -05:00
committed by GitHub
parent dd3a7a5145
commit 29e0497730
3 changed files with 97 additions and 32 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -586,7 +586,7 @@ export function openAdminPanel() {
<div class="form-group">
<label for="ooDocsOrigin">Document Server Origin:</label>
<input type="url" id="ooDocsOrigin" class="form-control" placeholder="e.g. http://192.168.1.61" />
<input type="url" id="ooDocsOrigin" class="form-control" placeholder="e.g. https://docs.example.com" />
<small class="text-muted">Must be reachable by your browser (for API.js) and by FileRise (for callbacks). Avoid “localhost”.</small>
</div>
@@ -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 dont 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 dont 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 <meta name="csp-nonce" content="...">, 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();