release(v1.8.1): fix(security,onlyoffice): sanitize DS origin; safe api.js/iframe probes; better UX placeholder
This commit is contained in:
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# 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)
|
## Changes 11/3/2025 (v1.8.0)
|
||||||
|
|
||||||
release(v1.8.0): feat(onlyoffice): first-class ONLYOFFICE integration (view/edit), admin UI, API, CSP helpers
|
release(v1.8.0): feat(onlyoffice): first-class ONLYOFFICE integration (view/edit), admin UI, API, CSP helpers
|
||||||
|
|||||||
@@ -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.
|
- 📝 **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.
|
- 🏷️ **Tags & Search:** Add color-coded tags and search by name, tag, uploader, or content. Advanced fuzzy search indexes metadata and file contents.
|
||||||
|
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ export function openAdminPanel() {
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="ooDocsOrigin">Document Server Origin:</label>
|
<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>
|
<small class="text-muted">Must be reachable by your browser (for API.js) and by FileRise (for callbacks). Avoid “localhost”.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -625,34 +625,77 @@ export function openAdminPanel() {
|
|||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
function ooClear(el) { while (el.firstChild) el.removeChild(el.firstChild); }
|
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) {
|
// Probes that don’t explode your state
|
||||||
return new Promise(resolve => {
|
async function ooProbeScript(docsOrigin) {
|
||||||
const src = docsOrigin.replace(/\/$/, '') + '/web-apps/apps/api/documents/api.js?probe=' + Date.now();
|
return new Promise(resolve => {
|
||||||
const s = document.createElement('script');
|
const base = getTrustedDocsOrigin(docsOrigin);
|
||||||
s.id = 'ooProbeScript';
|
if (!base) { resolve({ ok: false }); return; }
|
||||||
s.async = true;
|
|
||||||
s.src = src;
|
const src = buildOnlyOfficeApiUrl(base);
|
||||||
s.onload = () => { resolve({ ok: true }); setTimeout(() => s.remove(), 0); };
|
const s = document.createElement('script');
|
||||||
s.onerror = () => { resolve({ ok: false }); setTimeout(() => s.remove(), 0); };
|
s.id = 'ooProbeScript';
|
||||||
document.head.appendChild(s);
|
s.async = true;
|
||||||
});
|
s.src = src;
|
||||||
}
|
|
||||||
async function ooProbeFrame(docsOrigin, timeoutMs = 4000) {
|
// If you set a CSP nonce in a <meta name="csp-nonce" content="...">, attach it:
|
||||||
return new Promise(resolve => {
|
const nonce = document.querySelector('meta[name="csp-nonce"]')?.content;
|
||||||
const f = document.createElement('iframe');
|
if (nonce) s.setAttribute('nonce', nonce);
|
||||||
f.id = 'ooProbeFrame';
|
|
||||||
f.src = docsOrigin;
|
const cleanup = () => { try { s.remove(); } catch {} };
|
||||||
f.style.display = 'none';
|
|
||||||
let t = setTimeout(() => { cleanup(); resolve({ ok: false, timeout: true }); }, timeoutMs);
|
s.onload = () => { cleanup(); resolve({ ok: true }); };
|
||||||
function cleanup() { try { f.remove(); } catch { } clearTimeout(t); }
|
s.onerror = () => { cleanup(); resolve({ ok: false }); };
|
||||||
f.onload = () => { cleanup(); resolve({ ok: true }); };
|
|
||||||
f.onerror = () => { cleanup(); resolve({ ok: false }); };
|
// codeql[js/xss-through-dom]: the origin is validated (http/https, no creds),
|
||||||
document.body.appendChild(f);
|
// 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
|
// Main test runner
|
||||||
async function runOnlyOfficeTests() {
|
async function runOnlyOfficeTests() {
|
||||||
const spinner = document.getElementById('ooTestSpinner');
|
const spinner = document.getElementById('ooTestSpinner');
|
||||||
@@ -778,9 +821,10 @@ export function openAdminPanel() {
|
|||||||
const cspPreNgx = document.getElementById("ooCspSnippetNginx");
|
const cspPreNgx = document.getElementById("ooCspSnippetNginx");
|
||||||
|
|
||||||
function refreshCsp() {
|
function refreshCsp() {
|
||||||
const val = (ooDocsInput?.value || "").trim();
|
const raw = (ooDocsInput?.value || "").trim();
|
||||||
cspPre.textContent = buildCspApache(val);
|
const base = getTrustedDocsOrigin(raw) || raw; // fall back to raw so users see their input
|
||||||
cspPreNgx.textContent = buildCspNginx(val);
|
cspPre.textContent = buildCspApache(base);
|
||||||
|
cspPreNgx.textContent = buildCspNginx(base);
|
||||||
}
|
}
|
||||||
ooDocsInput?.addEventListener("input", refreshCsp);
|
ooDocsInput?.addEventListener("input", refreshCsp);
|
||||||
refreshCsp();
|
refreshCsp();
|
||||||
|
|||||||
Reference in New Issue
Block a user