Open-ended UI and MCP Apps
By the Syncfusion DevTutorials team · Last updated
A partner's MCP App arrives as full HTML. Host it in a sandboxed iframe with a CSP and message checks, so a malicious app cannot escape and a real one still works.
What Open-ended UI Is
Open-ended UI = someone else (another company's server, or a tool) sends a complete piece of interface, as HTML with scripts, and your app shows it.
In this lesson, you will host a courier's app inside the order-support app, and lock it down so a fake one cannot steal data, trick the user or approve a refund.
MCP App = an MCP extension where a tool returns a small web app as a resource, and the host shows it in a sandboxed iframe.
A courier offers a slot picker as an MCP App. It is handy: the customer picks a delivery time right inside the chat. But you do not control its HTML. A look-alike app can try to escape.
Press Play, or use Next to go one step at a time.
- Frame 1: no sandbox. Every attempt works: the frame is part of your page.
- Frame 2:
allow-scripts allow-same-origin. Still almost no protection. - Frames 3 to 5:
allow-scriptsalone, then a CSP, then message checks. Each layer blocks more.
Simple lesson: open-ended UI is someone else's code in your app. Contain it.
A Real-World Example
Think about a visitor in an office building.
- They get a badge that opens one meeting room, not every door.
- They can't take files out of the building.
- If they hand you a note "from the boss", you check it really is from the boss.
Hosting an MCP App works the same way:
- One meeting room = the iframe sandbox.
- No files out of the building = the Content Security Policy.
- Checking the note = validating messages from the frame.
Click the button. The Build tab loads the look-alike app with no sandbox.
- The Escape attempts box: each attempt, and whether it worked.
- Side effects: what the fake app got away with.
- Messages from the app: a fake "approve-refund" message.
Simple lesson: a frame without a sandbox is not a boundary.
What You Will Learn
In this lesson, you will learn:
- Why open-ended UI is the riskiest pattern.
- Which iframe sandbox flags contain untrusted HTML.
- Why two flags together undo the sandbox.
- How a CSP stops data leaving.
- How to check messages from the frame.
About the attacks in this lab
- The six escape attempts are simulated with written-out rules that match how browsers treat iframe sandbox flags.
- Real browsers enforce these rules themselves. Your job is to choose the right flags, and test your host in Chromium, Firefox and WebKit.
- The AI Security course goes deeper into attacks like these. The MCP course covers how MCP Apps are served.
The Iframe Sandbox
Sandbox = an iframe attribute that turns off powers by default. Each flag you add turns one power back on.
| sandbox value | Scripts run? | Can reach your page? | Use it? |
|---|---|---|---|
| (none) | Yes | Yes | Never, for untrusted HTML |
allow-scripts allow-same-origin | Yes | Yes, and it can remove its own sandbox | Never, for untrusted HTML |
allow-scripts | Yes | No: it gets its own origin | Yes |
"" (empty) | No | No | Only for HTML with no scripts |
The Dangerous Pair
allow-same-origin lets the frame keep your page's origin. With allow-scripts too, its script can reach your page, read cookies, and even take the sandbox off itself.
So for someone else's HTML, use allow-scripts on its own.
Too Tight Is Broken
An empty sandbox blocks scripts, so it blocks every attack. It also blocks the real app.
Step through both frames.
- Frame 1: with no scripts, the courier's real slot picker is blank.
- Frame 2: with
allow-scripts, a CSP and message checks, the fake app is contained and the real one works.
Simple lesson: allow-scripts alone: the app runs, your page is out of reach.
The Content Security Policy
CSP = rules that say where a page may load from and send data to.
The sandbox keeps the frame away from your page. But the frame can still call any website and send what it collected. A CSP in the frame's HTML blocks that:
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:">`;
<iframe title="Courier slot picker" sandbox="allow-scripts" srcDoc={csp + appHtml} />
default-src 'none' means: no network calls unless you allow them. Add only the origins the app really needs.
Check how your host serves the app.
- Some hosts serve MCP Apps from a separate domain with their own CSP header.
- Others use
srcdoc, like the example above. - Read your host's docs, and test that a call to an unknown site really fails.
Simple lesson: the sandbox keeps it in; the CSP keeps your data in.
Checking Messages
The frame talks to your app with postMessage. A fake app can post { type: "approve-refund" } and hope your page believes it.
Step through both frames.
- Frame 1: the fake app's message comes from the wrong origin, with an unknown type. Rejected.
- Frame 2: the real app's
slot-pickedmessage comes from the courier's origin. Accepted.
window.addEventListener("message", (e) => {
if (e.source !== frame.current?.contentWindow) return; // only our frame
if (!ALLOWED_TYPES.has(e.data?.type)) return; // only known messages
onMessage(e.data);
});
Never let a message from the frame trigger a risky action on its own. Show a preview and ask, as in Lesson 5.
Easy way to remember: Sandbox it, fence it, check its notes. allow-scripts, a CSP, and message checks.
Simple lesson: believe only known messages, from your own frame.
Lab Exercise: Lock Down the MCP App Host
Your task: change the Lesson 9 settings in the Build tab so both scenarios pass. Then press Check all scenarios.
Click the button and open the Build tab.
- iframe sandbox: pick the value that runs the app but keeps your page out of reach.
- CSP and message checks: turn both on.
- Both scenarios: the fake app must be contained, and the real one must still work.
Simple lesson: contain the app without breaking it.
What Changes per Framework
| Option | Open-ended UI |
|---|---|
| Hand-built React | An iframe with sandbox="allow-scripts", a CSP and a message check, as above |
| MCP Apps SDK | Host helpers for MCP App resources; still check its sandbox settings |
| CopilotKit | Shows MCP Apps carried over AG-UI |
| Claude and ChatGPT | Both hosts support MCP Apps for their own clients |
Build It in React
In the Code tab, an mcp-app event brings the app's page as HTML. Your host decides what that page may do. Three lines do most of the work:
<iframe ref={frame} sandbox="allow-scripts" srcDoc={CSP + app.html} />
if (e.source !== frame.current?.contentWindow) return; // only this frame
if (e.data?.type === "slot-picked") setSlot(e.data.slot); // only known messages
The look-alike app in the second scenario really tries to read your page, send data out and approve a refund. The checks report which tries worked.
TL;DR
- Open-ended UI = someone else's HTML and scripts in your app. Highest risk.
- MCP App = a small web app from an MCP tool, shown in a sandboxed iframe.
- Sandbox =
allow-scriptson its own. Never addallow-same-originfor untrusted HTML. - CSP = stops the frame sending data out.
- Message check = only known types, only from your frame.
- Too tight = an empty sandbox breaks the real app.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No sandbox | "It's from a partner, so it's fine." | Sandbox every third-party frame. | A compromised partner reads your page. |
| allow-scripts with allow-same-origin | "It needs both to work." | Use allow-scripts alone. | The frame removes its own sandbox. |
| No CSP | "The sandbox covers it." | Add a CSP so data can't leave. | Collected data is sent to another site. |
| Trusting every message | "Only our frame posts to us." | Check the source and the type. | A fake "approve-refund" is believed. |
| An empty sandbox | "Safest is best." | Allow scripts, contain the rest. | The real app is blank. |
Check Your Understanding
Exercises
What to Remember
- Open-ended UI is someone else's code in your app.
- Use
sandbox="allow-scripts"on its own. - Never add
allow-same-originto untrusted HTML that runs scripts. - Add a CSP so data cannot leave.
- Accept only known messages from your own frame.
- Test your host in every browser you support.
Further Reading
- MCP Apps, by the Model Context Protocol team: what MCP Apps are and how hosts show them.
- The iframe element and its sandbox attribute, on MDN: every sandbox flag explained.
Next: The AG-UI Channel, where you wire the agent and the UI over one event stream, with state updates that never erase what the user typed.