diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..955f0ce
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,97 @@
+# Security Policy
+
+Thanks for taking the time to look at ScreenTinker's security. The project
+is a one-person open-source effort, so response times reflect that — but
+reports are taken seriously and handled in good faith.
+
+## Reporting a vulnerability
+
+**Primary channel — GitHub Security Advisories (preferred):**
+
+[github.com/screentinker/screentinker/security/advisories/new](https://github.com/screentinker/screentinker/security/advisories/new)
+
+GitHub's private advisory flow keeps the report off public issues, lets us
+draft a fix collaboratively, and produces a CVE if appropriate. Use this
+unless you have a reason not to.
+
+**Fallback — email:**
+
+`support@bytetinker.net` (the maintainer's consultancy inbox; the domain
+intentionally differs from `screentinker.com` — it's the actively-monitored
+business address rather than a project-domain alias that might not have
+working mail delivery).
+
+Please include:
+- A description of the issue and its impact
+- Steps to reproduce (the more concrete, the better)
+- The commit SHA or release tag you observed it on
+- Any proof-of-concept code or payload, if you have one
+
+## Response timeline
+
+I aim to acknowledge reports within **3–5 business days** and update with a
+triage assessment within **10 business days**. If you haven't heard back
+in that window, please feel free to nudge — life happens, and reports
+occasionally slip past.
+
+Fix timelines depend on severity, complexity, and whether the issue is on
+the hosted instance (screentinker.com) or affects self-hosted deployments
+too. Critical issues affecting the hosted instance generally get same-week
+turnaround.
+
+## In scope
+
+Reports about the following are welcome and treated as security issues:
+
+- **Authentication / session bypass** (e.g. JWT forgery, login bypass,
+ privilege escalation)
+- **Multi-tenancy boundary violations** (one workspace's data leaking into
+ another, organization-level isolation breaks)
+- **XSS in widget rendering or admin UI** (e.g. unsandboxed widget content,
+ unescaped user input in dashboard surfaces)
+- **CSRF** on state-changing endpoints
+- **SQL injection** (deviations from parameterized queries are reportable)
+- **Server-side request forgery** (SSRF) via widget URLs, content uploads,
+ webhook handlers, or similar
+- **Insecure direct object reference** (accessing a resource by ID without
+ the proper tenancy gate)
+
+## Out of scope
+
+The following are acknowledged but not treated as in-scope security
+issues for this project:
+
+- **Denial of service via excessive resource usage** (uploading large
+ files, opening many sockets, etc.) — operational concerns, not security
+ vulnerabilities. Rate limits exist where it matters most.
+- **Social engineering** of the maintainer or other users
+- **Misconfigurations of self-hosted instances** (e.g. exposing the server
+ to the internet without TLS, weak JWT secrets, default passwords). The
+ README documents recommended configuration; deviations are the operator's
+ responsibility.
+- **Vulnerabilities in third-party dependencies** (Express, better-sqlite3,
+ socket.io, etc.) — please report those upstream. If a dependency CVE
+ affects ScreenTinker in a non-obvious way, that's worth flagging here too.
+- **Reports generated by automated scanners** with no manual triage or
+ proof-of-concept (e.g. "your /robots.txt is missing" — not what this
+ project worries about)
+
+## Coordinated disclosure
+
+Please **wait until a fix has shipped to the hosted instance and
+origin/main before public disclosure**. I'll keep you in the loop on
+timing and confirm when it's safe to publish. For most issues that
+window is a few weeks at most; if it stretches longer, that's a signal
+something is more complex than expected and we'll coordinate.
+
+If you find a critical issue that's being actively exploited (or you
+believe might be), please say so in the report — I'll prioritize
+accordingly.
+
+## Acknowledgments
+
+If you'd like to be credited for a report, I'm happy to acknowledge you
+by name in release notes and (when applicable) in the GitHub advisory
+itself. Let me know in your report whether you'd like credit and how
+you'd like to be named. Anonymous reports are also welcome — no credit
+is required.
diff --git a/frontend/js/views/widgets.js b/frontend/js/views/widgets.js
index c7e17d2..d2e9526 100644
--- a/frontend/js/views/widgets.js
+++ b/frontend/js/views/widgets.js
@@ -114,7 +114,7 @@ function showPreviewModal(html) {
${t('widget.preview_title')}
-
+
`;
document.body.appendChild(overlay);
// srcdoc resolves relative URLs against about:srcdoc, so inject pointing to our origin
diff --git a/server/player/index.html b/server/player/index.html
index 6c9fd8e..11c5815 100644
--- a/server/player/index.html
+++ b/server/player/index.html
@@ -1434,6 +1434,9 @@
iframe.src = `${serverUrl}/api/widgets/${item.widget_id}/render`;
iframe.style.cssText = 'width:100%;height:100%;border:none;background:#000';
iframe.allow = 'autoplay; fullscreen';
+ // Sandbox into a unique origin so widget scripts can't read window.parent
+ // state (localStorage / JWT). allow-scripts keeps inline widget code running.
+ iframe.setAttribute('sandbox', 'allow-scripts');
mount.appendChild(iframe);
if (!isFollower) advanceTimer = setTimeout(nextItem, (item.duration_sec || 30) * 1000);
}
@@ -1459,6 +1462,9 @@
if (zone.zone_type === 'widget' && assignment.widget_id) {
const iframe = document.createElement('iframe');
iframe.src = `${config.serverUrl}/api/widgets/${assignment.widget_id}/render`;
+ // Sandbox into a unique origin so widget scripts can't read window.parent
+ // state (localStorage / JWT). allow-scripts keeps inline widget code running.
+ iframe.setAttribute('sandbox', 'allow-scripts');
div.appendChild(iframe);
} else if (isYoutubeZone) {
createYoutubeEmbed(src, assignment, div);