Skip to content

Commit

Permalink
Add cross-origin window and location wrappers (#291)
Browse files Browse the repository at this point in the history
Closes dart-lang/sdk#54443
Closes #247
Closes dart-lang/sdk#54938

Since cross-origin objects have limitations around
access, wrappers are introduced to do the only safe
operations. Extension methods are added to get instances
of these wrappers.
  • Loading branch information
srujzs authored Sep 24, 2024
1 parent f1acb17 commit 8501740
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 0 deletions.
5 changes: 5 additions & 0 deletions web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
`switch`ed over.
- Add an extension `responseHeaders` to `XMLHttpRequest`.
- Correctly namespace `WebAssembly` types.
- Added `CrossOriginWindow` and `CrossOriginLocation` wrappers for cross-origin
windows and locations, respectively, that can be accessed through
`HTMLIFrameElement.contentWindowCrossOrigin`, `Window.openCrossOrigin`,
`Window.openerCrossOrigin`, `Window.topCrossOrigin`,
and `Window.parentCrossOrigin`.

## 1.0.0

Expand Down
1 change: 1 addition & 0 deletions web/lib/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'dart:js_interop_unsafe';
import 'dom.dart';
import 'helpers/lists.dart';

export 'helpers/cross_origin.dart' show CrossOriginLocation, CrossOriginWindow;
export 'helpers/enums.dart';
export 'helpers/events/events.dart';
export 'helpers/events/providers.dart';
Expand Down
203 changes: 203 additions & 0 deletions web/lib/src/helpers/cross_origin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:js_interop';

import '../dom.dart' show HTMLIFrameElement, Location, Window;

// The Dart runtime does not allow this to be typed as any better than `JSAny?`.
extension type _CrossOriginWindow(JSAny? any) {
external bool get closed;
external int get length;
// While you can set the location to a string value, this is the same as
// `location.href`, so we only allow the getter to avoid a
// `getter_not_subtype_setter_types` error.
external JSAny? get location;
external JSAny? get opener;
external JSAny? get parent;
external JSAny? get top;
// `frames`, `self`, and `window` are all supported for cross-origin windows,
// but simply return the calling window, so there's no use in supporting them
// for interop.
external void blur();
external void close();
external void focus();
external void postMessage(
JSAny? message, [
JSAny optionsOrTargetOrigin,
JSArray<JSObject> transfer,
]);
}

// The Dart runtime does not allow this to be typed as any better than `JSAny?`.
extension type _CrossOriginLocation(JSAny? any) {
external void replace(String url);
external set href(String value);
}

/// A safe wrapper for a cross-origin window.
///
/// Since cross-origin access is limited by the browser, the Dart runtime can't
/// provide a type for or null-assert the cross-origin window. To safely
/// interact with the cross-origin window, use this wrapper instead.
///
/// The `dart:html` equivalent is `_DOMWindowCrossFrame`.
///
/// Only includes allowed APIs from the W3 spec located here:
/// https://html.spec.whatwg.org/multipage/nav-history-apis.html#crossoriginproperties-(-o-)
/// Some browsers may provide more access.
class CrossOriginWindow {
CrossOriginWindow._(JSAny? o) : _window = _CrossOriginWindow(o);

static CrossOriginWindow? _create(JSAny? o) {
if (o == null) return null;
return CrossOriginWindow._(o);
}

final _CrossOriginWindow _window;

/// The [Window.closed] value of this cross-origin window.
bool get closed => _window.closed;

/// The [Window.length] value of this cross-origin window.
int get length => _window.length;

/// A [CrossOriginLocation] wrapper of the [Window.location] value of this
/// cross-origin window.
CrossOriginLocation? get location =>
CrossOriginLocation._create(_window.location);

/// A [CrossOriginWindow] wrapper of the [Window.opener] value of this
/// cross-origin window.
CrossOriginWindow? get opener => _create(_window.opener);

/// A [CrossOriginWindow] wrapper of the [Window.top] value of this
/// cross-origin window.
CrossOriginWindow? get parent => _create(_window.parent);

/// A [CrossOriginWindow] wrapper of the [Window.parent] value of this
/// cross-origin window.
CrossOriginWindow? get top => _create(_window.top);

/// Calls [Window.blur] on this cross-origin window.
void blur() => _window.blur();

/// Calls [Window.close] on this cross-origin window.
void close() => _window.close();

/// Calls [Window.focus] on this cross-origin window.
void focus() => _window.focus();

/// Calls [Window.postMessage] on this cross-origin window with the given
/// [message], [optionsOrTargetOrigin] if not `null`, and [transfer] if not
/// `null`.
void postMessage(
JSAny? message, [
JSAny? optionsOrTargetOrigin,
JSArray<JSObject>? transfer,
]) {
if (optionsOrTargetOrigin == null) {
_window.postMessage(message);
} else if (transfer == null) {
_window.postMessage(message, optionsOrTargetOrigin);
} else {
_window.postMessage(message, optionsOrTargetOrigin, transfer);
}
}

/// The unsafe window value that this wrapper wraps that should only ever be
/// typed as <code>[JSAny]?</code>.
///
/// > [!NOTE]
/// > This is only intended to be passed to an interop member that expects a
/// > <code>[JSAny]?</code>. Safety for any other operations is not
/// > guaranteed.
JSAny? get unsafeWindow => _window.any;
}

/// A safe wrapper for a cross-origin location obtained through a cross-origin
/// window.
///
/// Since cross-origin access is limited by the browser, the Dart runtime can't
/// provide a type for or null-assert the cross-origin location. To safely
/// interact with the cross-origin location, use this wrapper instead.
///
/// The `dart:html` equivalent is `_LocationCrossFrame`.
///
/// Only includes allowed APIs from the W3 spec located here:
/// https://html.spec.whatwg.org/multipage/nav-history-apis.html#crossoriginproperties-(-o-)
/// Some browsers may provide more access.
class CrossOriginLocation {
CrossOriginLocation._(JSAny? o) : _location = _CrossOriginLocation(o);

static CrossOriginLocation? _create(JSAny? o) {
if (o == null) return null;
return CrossOriginLocation._(o);
}

final _CrossOriginLocation _location;

/// Sets the [Location.href] value of this cross-origin location to [value].
set href(String value) => _location.href = value;

/// Calls [Location.replace] on this cross-origin location with the given
/// [url].
void replace(String url) => _location.replace(url);

/// The unsafe location value that this wrapper wraps that should only ever be
/// typed as <code>[JSAny]?</code>.
///
/// > [!NOTE]
/// > This is only intended to be passed to an interop member that expects a
/// > <code>[JSAny]?</code>. Safety for any other operations is not
/// > guaranteed.
JSAny? get unsafeLocation => _location.any;
}

extension CrossOriginContentWindowExtension on HTMLIFrameElement {
@JS('contentWindow')
external JSAny? get _contentWindow;

/// A [CrossOriginWindow] wrapper of the [HTMLIFrameElement.contentWindow]
/// value of this `iframe`.
CrossOriginWindow? get contentWindowCrossOrigin =>
CrossOriginWindow._create(_contentWindow);
}

/// Safe alternatives to common [Window] members that can return cross-origin
/// windows.
///
/// By default, the Dart web compilers are not sensitive to cross-origin
/// objects, and therefore same-origin policy errors may be triggered when
/// type-checking. Use these members instead to safely interact with such
/// objects.
extension CrossOriginWindowExtension on Window {
@JS('open')
external JSAny? _open(String url);

/// A [CrossOriginWindow] wrapper of the value returned from calling
/// [Window.open] with [url].
CrossOriginWindow? openCrossOrigin(String url) =>
CrossOriginWindow._create(_open(url));
@JS('opener')
external JSAny? get _opener;

/// A [CrossOriginWindow] wrapper of the [Window.opener] value of this
/// cross-origin window.
CrossOriginWindow? get openerCrossOrigin =>
CrossOriginWindow._create(_opener);
@JS('parent')
external JSAny? get _parent;

/// A [CrossOriginWindow] wrapper of the [Window.parent] value of this
/// cross-origin window.
CrossOriginWindow? get parentCrossOrigin =>
CrossOriginWindow._create(_parent);
@JS('top')
external JSAny? get _top;

/// A [CrossOriginWindow] wrapper of the [Window.top] value of this
/// cross-origin window.
CrossOriginWindow? get topCrossOrigin => CrossOriginWindow._create(_top);
}
3 changes: 3 additions & 0 deletions web/lib/src/helpers/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import 'dart:math' show Point;
import '../dom.dart';
import 'lists.dart';

export 'cross_origin.dart'
show CrossOriginContentWindowExtension, CrossOriginWindowExtension;

extension HTMLCanvasElementGlue on HTMLCanvasElement {
CanvasRenderingContext2D get context2D =>
getContext('2d') as CanvasRenderingContext2D;
Expand Down
79 changes: 79 additions & 0 deletions web/test/helpers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import 'dart:js_interop';
import 'package:test/test.dart';
import 'package:web/web.dart';

@JS('Object.is')
external bool _is(JSAny? a, JSAny? b);

void main() {
test('instanceOfString works with package:web types', () {
final div = document.createElement('div') as JSObject;
Expand Down Expand Up @@ -55,4 +58,80 @@ void main() {
),
);
});

test('cross-origin windows and locations can be accessed safely', () {
// TODO(https://github.com/dart-lang/test/issues/2282): For some reason,
// running `dart test` doesn't flag violations of same-origin policy,
// allowing any unsafe accesses. When tested with `--pause-after-load` and
// single stepped, however, the test correctly flags violations. Figure out
// why and make this test always respect same-origin policy. Add some tests
// to ensure that violations are being handled properly.
const url = 'https://www.google.com';
const url2 = 'https://www.example.org';

void testCommon(CrossOriginWindow crossOriginWindow) {
expect(crossOriginWindow.length, 0);
expect(crossOriginWindow.closed, false);
// We can't add an event listener on a cross-origin window, so just test
// that a message can be sent without any errors.
crossOriginWindow.postMessage('hello world'.toJS);
crossOriginWindow.postMessage('hello world'.toJS, url.toJS);
crossOriginWindow.postMessage('hello world'.toJS, url.toJS, JSArray());
crossOriginWindow.location!.replace(url2);
crossOriginWindow.location!.href = url;
crossOriginWindow.blur();
crossOriginWindow.focus();
crossOriginWindow.close();
}

final openedWindow = window.openCrossOrigin(url)!;
// Use `Object.is` to test that values can be passed to interop.
expect(_is(openedWindow.opener!.unsafeWindow, window), true);
expect(
_is(openedWindow.top!.unsafeWindow, openedWindow.unsafeWindow), true);
expect(_is(openedWindow.parent!.unsafeWindow, openedWindow.unsafeWindow),
true);
expect(_is(openedWindow.opener!.location!.unsafeLocation, window.location),
true);
expect(
_is(openedWindow.opener!.parent?.unsafeWindow,
window.parentCrossOrigin?.unsafeWindow),
true);
expect(
_is(openedWindow.opener!.top?.unsafeWindow,
window.topCrossOrigin?.unsafeWindow),
true);
expect(openedWindow.opener!.opener?.unsafeWindow,
window.openerCrossOrigin?.unsafeWindow);
testCommon(openedWindow);
expect(openedWindow.closed, true);

final iframe = HTMLIFrameElement();
iframe.src = url;
document.body!.append(iframe);
final contentWindow = iframe.contentWindowCrossOrigin!;
expect(contentWindow.opener, null);
expect(
_is(contentWindow.top?.unsafeWindow,
window.topCrossOrigin?.unsafeWindow),
true);
expect(_is(contentWindow.parent!.unsafeWindow, window), true);
expect(_is(contentWindow.parent!.location!.unsafeLocation, window.location),
true);
expect(
_is(contentWindow.parent!.parent?.unsafeWindow,
window.parentCrossOrigin?.unsafeWindow),
true);
expect(
_is(contentWindow.parent!.top?.unsafeWindow,
window.topCrossOrigin?.unsafeWindow),
true);
expect(
_is(contentWindow.parent!.opener?.unsafeWindow,
window.openerCrossOrigin?.unsafeWindow),
true);
testCommon(contentWindow);
// `close` on a `contentWindow` does nothing.
expect(contentWindow.closed, false);
});
}

0 comments on commit 8501740

Please sign in to comment.