mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-18 20:22:42 -06:00
security(widgets): add sandbox="allow-scripts" to widget iframes
Addresses the primary finding from the May 27 security report (issue #8): the admin widget preview modal (frontend/js/views/widgets.js) and the web player widget renderer (server/player/index.html, 2 sites) loaded user-authored widget HTML into unsandboxed iframes. Same-origin scripts in the widget content could access window.parent.localStorage and exfiltrate the JWT. sandbox="allow-scripts" without allow-same-origin sandboxes the widget into a unique origin: inline scripts (clock, RSS, weather widgets) continue to work, but parent-origin access and same-origin requests are blocked. Verified via Playwright probe against all 6 widget types in the dev DB (clock, rss, social, text, weather, webpage): each renders correctly under the new sandbox and contentDocument access from the parent is blocked (opaque-origin enforcement working). Admin preview unchanged in appearance; player display unchanged. Webpage widget (server/routes/widgets.js) sandbox tightening (drop allow-same-origin) is a separate forthcoming commit - needs test against real embed URLs since some sites rely on same-origin behavior. The sandbox-attribute intersection rule means today's outer-iframe sandbox will cascade and strip allow-same-origin from the webpage widget's inner iframe too; accepted as a narrow cosmetic regression (cookies/localStorage stripped for embedded sites) until the deliberate inner-iframe handling ships. SECURITY.md added with reporting process (GitHub Security Advisories primary, support@bytetinker.net fallback) and scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
159a36ed99
commit
fe36c8c4b9
97
SECURITY.md
Normal file
97
SECURITY.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -114,7 +114,7 @@ function showPreviewModal(html) {
|
||||||
<strong style="color:var(--text-primary)">${t('widget.preview_title')}</strong>
|
<strong style="color:var(--text-primary)">${t('widget.preview_title')}</strong>
|
||||||
<button class="btn btn-secondary btn-sm" id="pvClose">${t('widget.close')}</button>
|
<button class="btn btn-secondary btn-sm" id="pvClose">${t('widget.close')}</button>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="pvIframe" style="flex:1;width:100%;border:0;background:#000"></iframe>
|
<iframe id="pvIframe" sandbox="allow-scripts" style="flex:1;width:100%;border:0;background:#000"></iframe>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
// srcdoc resolves relative URLs against about:srcdoc, so inject <base> pointing to our origin
|
// srcdoc resolves relative URLs against about:srcdoc, so inject <base> pointing to our origin
|
||||||
|
|
|
||||||
|
|
@ -1434,6 +1434,9 @@
|
||||||
iframe.src = `${serverUrl}/api/widgets/${item.widget_id}/render`;
|
iframe.src = `${serverUrl}/api/widgets/${item.widget_id}/render`;
|
||||||
iframe.style.cssText = 'width:100%;height:100%;border:none;background:#000';
|
iframe.style.cssText = 'width:100%;height:100%;border:none;background:#000';
|
||||||
iframe.allow = 'autoplay; fullscreen';
|
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);
|
mount.appendChild(iframe);
|
||||||
if (!isFollower) advanceTimer = setTimeout(nextItem, (item.duration_sec || 30) * 1000);
|
if (!isFollower) advanceTimer = setTimeout(nextItem, (item.duration_sec || 30) * 1000);
|
||||||
}
|
}
|
||||||
|
|
@ -1459,6 +1462,9 @@
|
||||||
if (zone.zone_type === 'widget' && assignment.widget_id) {
|
if (zone.zone_type === 'widget' && assignment.widget_id) {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.src = `${config.serverUrl}/api/widgets/${assignment.widget_id}/render`;
|
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);
|
div.appendChild(iframe);
|
||||||
} else if (isYoutubeZone) {
|
} else if (isYoutubeZone) {
|
||||||
createYoutubeEmbed(src, assignment, div);
|
createYoutubeEmbed(src, assignment, div);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue