I'm embedding a JavaScript-based web editor (served from http://localhost:3005) into a Flutter Web app (running on http://localhost:5000). I'm using Dart's JS interop (dart:js_interop) to communicate between the Flutter parent page and the embedded iframe via window.postMessage.
When attempting to send a message from Flutter (Dart) to the iframe, I consistently encounter a SecurityError:
SecurityError: Blocked a frame with origin "http://localhost:5000" from accessing a cross-origin frame.
The iframe correctly loads, and messages from the iframe to Flutter (using parent.postMessage) work without issue. However, messages from Flutter to the iframe are blocked by the browser due to cross-origin restrictions.
Goal: Establish two-way communication between Flutter (Dart) and a cross-origin iframe using postMessage without encountering security errors.
Expected Result: Dart successfully sends messages to the iframe without security errors.
JavaScript iframe receives and processes these messages correctly.
Actual Result: I receive the following error:
SecurityError: Blocked a frame with origin "http://localhost:5000" from accessing a cross-origin frame.
Minimal Reproducible Example
1. Flutter Web code (main.dart & webview.dart)
import 'dart:convert';
import 'dart:js_interop';
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
import 'dart:ui_web' as ui_web;
@JS()
@staticInterop
extension type WindowExtension._(JSObject _) implements JSObject {
external void postMessage(JSAny message, JSString targetOrigin);
}
extension WindowCasting on web.Window {
WindowExtension get ext => this as WindowExtension;
}
class EditorController {
web.HTMLIFrameElement? _iframe;
void setIframe(web.HTMLIFrameElement iframe) {
_iframe = iframe;
}
void sendMessage(dynamic msg) {
final jsonMsg = jsonEncode(msg);
final cw = _iframe?.contentWindow;
if (cw == null) {
debugPrint('Cannot send—window is null');
return;
}
try {
// targetOrigin matches editor server
cw.ext.postMessage(jsonMsg.toJS, 'http://localhost:3005'.toJS);
debugPrint('Message sent: $jsonMsg');
} catch (e) {
debugPrint('Error sending message: $e');
}
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final EditorController ctrl = EditorController();
@override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed:
() => ctrl.sendMessage({'type': 'test', 'data': 'Hello'}),
child: Text('Send Message'),
),
Expanded(child: EditorWebView(controller: ctrl)),
],
),
),
);
}
}
class EditorWebView extends StatefulWidget {
final EditorController controller;
const EditorWebView({required this.controller, super.key});
@override
State<EditorWebView> createState() => _EditorWebViewState();
}
class _EditorWebViewState extends State<EditorWebView> {
late final String viewId;
late final web.HTMLIFrameElement iframe;
@override
void initState() {
super.initState();
viewId = 'editor-iframe-${UniqueKey()}';
iframe =
web.document.createElement('iframe') as web.HTMLIFrameElement
..id = viewId
..style.border = 'none'
..style.width = '100%'
..style.height = '100%'
..src = 'http://localhost:3005/editor.html';
ui_web.platformViewRegistry.registerViewFactory(viewId, (i) => iframe);
widget.controller.setIframe(iframe);
}
@override
Widget build(BuildContext c) =>
SizedBox.expand(child: HtmlElementView(viewType: viewId));
}
2. Editor HTML (public/editor.html)
<!DOCTYPE html>
<html><head><title>Mock Editor</title></head>
<body>
<script>
// Log incoming messages
window.addEventListener('message', e => {
console.log('Editor received:', e.data, 'from', e.origin);
});
// Notify parent when loaded
parent.postMessage({type:'iframeLoaded'}, 'http://localhost:5000');
console.log('Editor iframe loaded, sent ready message');
</script>
</body></html>
3. Express server (server.ts)
import express from 'express';
import cors from 'cors';
import path from 'path';
const app = express();
app.use(cors({ origin: 'http://localhost:5000', credentials: true }));
app.use((req, res, next) => {
res.header('Content-Security-Policy',
`default-src 'self'; frame-ancestors http://localhost:5000;`
);
next();
});
app.use(express.static(path.join(__dirname,'public')));
app.listen(3005,()=>console.log('Editor @ http://localhost:3005'));
What I've Already Tried:
Using dart:js_interop and explicitly casting to bypass dynamic property lookups.
Result: No change, the security error still appears.
Setting the iframe sandbox attribute to various combinations (allow-scripts, allow-same-origin).
Result: Browser warnings and still blocked cross-origin access.
Using js_util.callMethod() from dart:js_util instead of direct interop.
Result: Works, but dart:js_util and dart:html are depricated and I dont want to use them.
What am I missing?
How can I properly enable cross-origin communication using window.postMessage from Dart (Flutter Web) to a JavaScript iframe hosted on a different origin without encountering a SecurityError?
dart:htmlworks perfectly, but as you said: it's deprecated so it's a poor solution. The moment I swapped over topackage:webandjs_interopeverything broke with permissions issues. Zero other difference in the code. Have you found any other solutions on your end?