diff --git a/frontend/index.html b/frontend/index.html
index 94a6eac..7c213a1 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -157,7 +157,7 @@
diff --git a/frontend/js/app.js b/frontend/js/app.js
index d6def43..679a308 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -304,3 +304,12 @@ if (isAuthenticated()) {
}
window.addEventListener('hashchange', route);
route();
+
+// Close-modal buttons (replaces inline onclick handlers — required for CSP).
+document.addEventListener('click', (e) => {
+ const closer = e.target.closest('[data-close-modal]');
+ if (!closer) return;
+ const id = closer.dataset.closeModal;
+ const modal = document.getElementById(id);
+ if (modal) modal.style.display = 'none';
+});
diff --git a/server/server.js b/server/server.js
index aa20bae..6db4d16 100644
--- a/server/server.js
+++ b/server/server.js
@@ -29,27 +29,100 @@ if (hasSsl) {
server = http.createServer(app);
}
+// Socket.IO CORS is checked via the same corsOriginCheck function defined below
+// (after config is loaded). Hoisted into a closure so we can reference it before
+// the function is defined — at first connection time, corsOriginCheck exists.
const io = new Server(server, {
- cors: { origin: '*' },
+ cors: {
+ origin: (origin, cb) => corsOriginCheck(origin, cb),
+ credentials: true,
+ },
maxHttpBufferSize: 10 * 1024 * 1024 // 10MB for screenshot uploads
});
// Middleware
const helmet = require('helmet');
+
+// CSP applies to the dashboard / app pages only. Widget and kiosk renders are
+// publicly accessed by devices and intentionally use inline scripts/styles —
+// they're served from /api/widgets/:id/render and /api/kiosk/:id/render and
+// skip the CSP layer below via path-based opt-out.
+//
+// scriptSrc 'self' blocks