1

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?

1
  • I recently ran into the same error only in reverse: putting a flutter app into a traditional web app and using messaging passing to go between them. using dart:html works perfectly, but as you said: it's deprecated so it's a poor solution. The moment I swapped over to package:web and js_interop everything broke with permissions issues. Zero other difference in the code. Have you found any other solutions on your end? Commented Jul 23 at 20:41

1 Answer 1

0

I modified your sendMessage code:

  void sendMessage(dynamic msg) {
    final jsonMsg = jsonEncode(msg);
    final cw = _iframe?.contentWindowCrossOrigin;
    if (cw == null) {
      debugPrint('Cannot send—window is null');
      return;
    }
    try {
      // targetOrigin matches editor server
      cw.postMessage(jsonMsg.toJS, 'http://localhost:3005'.toJS);
      debugPrint('Message sent: $jsonMsg');
    } catch (e) {
      debugPrint('Error sending message: $e');
    }
  }

That enabled your code sample to stop having the
SecurityError: Blocked a frame with origin "http://localhost:5000" from accessing a cross-origin frame. for me:

Output from my console:

js_primitives.dart:28 Message sent: {"type":"test","data":"Hello"}

You can read into what exactly it's doing/why that method exists from the web package at:
lib/src/helpers/cross_origin.dart in the package:web package (command+click on the contentWindowCrossOrigin getter - that's where it's from).

My understanding is that the new web package is a lot stricter about windows and types and so the errors come up on the new system whereas the old dart:html got away with it under some circumstances on some browsers. However, the new package provides these new safer types specifically for cross-origin interops.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.