LibWeb: Parse and propagate the iframe sandbox attribute

This commit is contained in:
Luke Wilde 2024-12-06 14:53:03 +00:00 committed by Alexander Kalenik
commit 40bb50ac60
Notes: github-actions[bot] 2025-08-07 17:26:35 +00:00
51 changed files with 1155 additions and 12 deletions

View file

@ -555,6 +555,7 @@ set(SOURCES
HTML/PromiseRejectionEvent.cpp
HTML/RadioNodeList.cpp
HTML/RenderingThread.cpp
HTML/SandboxingFlagSet.cpp
HTML/Scripting/Agent.cpp
HTML/Scripting/ClassicScript.cpp
HTML/Scripting/Environments.cpp

View file

@ -13,6 +13,7 @@
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/BrowsingContextGroup.h>
#include <LibWeb/HTML/HTMLDocument.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
@ -487,11 +488,28 @@ bool BrowsingContext::is_familiar_with(BrowsingContext const& other) const
return false;
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#snapshotting-target-snapshot-params
SandboxingFlagSet determine_the_creation_sandboxing_flags(BrowsingContext const&, GC::Ptr<DOM::Element>)
// https://html.spec.whatwg.org/multipage/browsers.html#determining-the-creation-sandboxing-flags
SandboxingFlagSet determine_the_creation_sandboxing_flags(BrowsingContext const& browsing_context, GC::Ptr<DOM::Element> embedder)
{
// FIXME: Populate this once we have the proper flag sets on BrowsingContext
return {};
// To determine the creation sandboxing flags for a browsing context browsing context, given null or an element
// embedder, return the union of the flags that are present in the following sandboxing flag sets:
SandboxingFlagSet sandboxing_flags {};
// - If embedder is null, then: the flags set on browsing context's popup sandboxing flag set.
if (!embedder) {
sandboxing_flags |= browsing_context.popup_sandboxing_flag_set();
} else {
// - If embedder is an element, then: the flags set on embedder's iframe sandboxing flag set.
if (is<HTMLIFrameElement>(embedder.ptr())) {
auto const& iframe_element = static_cast<HTMLIFrameElement const&>(*embedder);
sandboxing_flags |= iframe_element.iframe_sandboxing_flag_set();
}
// - If embedder is an element, then: the flags set on embedder's node document's active sandboxing flag set.
sandboxing_flags |= embedder->document().active_sandboxing_flag_set();
}
return sandboxing_flags;
}
bool BrowsingContext::has_navigable_been_destroyed() const

View file

@ -53,6 +53,11 @@ void HTMLIFrameElement::attribute_changed(FlyString const& name, Optional<String
{
Base::attribute_changed(name, old_value, value, namespace_);
if (name == HTML::AttributeNames::sandbox) {
if (m_sandbox)
m_sandbox->associated_attribute_changed(value.value_or(String {}));
}
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element:process-the-iframe-attributes-2
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element:process-the-iframe-attributes-3
// Whenever an iframe element with a non-null content navigable has its srcdoc attribute set, changed, or removed,
@ -62,6 +67,21 @@ void HTMLIFrameElement::attribute_changed(FlyString const& name, Optional<String
if (m_content_navigable) {
if (name == AttributeNames::srcdoc || (name == AttributeNames::src && !has_attribute(AttributeNames::srcdoc)))
process_the_iframe_attributes();
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element:iframe-sandboxing-flag-set-2
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element:iframe-sandboxing-flag-set-3
// When an iframe element's sandbox attribute is set or changed while it has a non-null content navigable, the
// user agent must parse the sandboxing directive given the attribute's value and the iframe element's iframe
// sandboxing flag set.
// When an iframe element's sandbox attribute is removed while it has a non-null content navigable, the user
// agent must empty the iframe element's iframe sandboxing flag set.
if (name == AttributeNames::sandbox) {
if (value.has_value()) {
m_iframe_sandboxing_flag_set = parse_a_sandboxing_directive(value.value());
} else {
m_iframe_sandboxing_flag_set = {};
}
}
}
if (name == HTML::AttributeNames::width || name == HTML::AttributeNames::height) {
@ -89,8 +109,13 @@ void HTMLIFrameElement::post_connection()
// The iframe HTML element post-connection steps, given insertedNode, are:
// 1. Create a new child navigable for insertedNode.
MUST(create_new_child_navigable(GC::create_function(realm().heap(), [this] {
// FIXME: 2. If insertedNode has a sandbox attribute, then parse the sandboxing directive given the attribute's
// 2. If insertedNode has a sandbox attribute, then parse the sandboxing directive given the attribute's
// value and insertedNode's iframe sandboxing flag set.
if (has_attribute(AttributeNames::sandbox)) {
auto sandbox_attribute = attribute(AttributeNames::sandbox);
VERIFY(sandbox_attribute.has_value());
m_iframe_sandboxing_flag_set = parse_a_sandboxing_directive(sandbox_attribute.value());
}
// 3. Process the iframe attributes for insertedNode, with initialInsertion set to true.
process_the_iframe_attributes(InitialInsertion::Yes);

View file

@ -33,6 +33,8 @@ public:
GC::Ref<DOM::DOMTokenList> sandbox();
SandboxingFlagSet iframe_sandboxing_flag_set() const { return m_iframe_sandboxing_flag_set; }
virtual void visit_edges(Cell::Visitor&) override;
private:
@ -64,6 +66,9 @@ private:
Optional<HighResolutionTime::DOMHighResTimeStamp> m_pending_resource_start_time = {};
GC::Ptr<DOM::DOMTokenList> m_sandbox;
// https://html.spec.whatwg.org/multipage/browsers.html#iframe-sandboxing-flag-set
SandboxingFlagSet m_iframe_sandboxing_flag_set {};
};
void run_iframe_load_event_steps(HTML::HTMLIFrameElement&);

View file

@ -0,0 +1,107 @@
/*
* Copyright (c) 2024, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/Infra/CharacterTypes.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/browsers.html#parse-a-sandboxing-directive
SandboxingFlagSet parse_a_sandboxing_directive(String const& input)
{
// 1. Split input on ASCII whitespace, to obtain tokens.
auto lowercase_input = input.to_ascii_lowercase();
auto tokens = lowercase_input.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
// 2. Let output be empty.
SandboxingFlagSet output {};
// 3. Add the following flags to output:
// - The sandboxed navigation browsing context flag.
output |= SandboxingFlagSet::SandboxedNavigation;
// - The sandboxed auxiliary navigation browsing context flag, unless tokens contains the allow-popups keyword.
if (!tokens.contains_slow("allow-popups"sv))
output |= SandboxingFlagSet::SandboxedAuxiliaryNavigation;
// - The sandboxed top-level navigation without user activation browsing context flag, unless tokens contains the
// allow-top-navigation keyword.
if (!tokens.contains_slow("allow-top-navigation"sv))
output |= SandboxingFlagSet::SandboxedTopLevelNavigationWithoutUserActivation;
// - The sandboxed top-level navigation with user activation browsing context flag, unless tokens contains either
// the allow-top-navigation-by-user-activation keyword or the allow-top-navigation keyword.
// Spec Note: This means that if the allow-top-navigation is present, the allow-top-navigation-by-user-activation
// keyword will have no effect. For this reason, specifying both is a document conformance error.
if (!tokens.contains_slow("allow-top-navigation"sv) && !tokens.contains_slow("allow-top-navigation-by-user-activation"sv))
output |= SandboxingFlagSet::SandboxedTopLevelNavigationWithUserActivation;
// - The sandboxed origin browsing context flag, unless the tokens contains the allow-same-origin keyword.
// Spec Note: The allow-same-origin keyword is intended for two cases.
//
// First, it can be used to allow content from the same site to be sandboxed to disable scripting,
// while still allowing access to the DOM of the sandboxed content.
//
// Second, it can be used to embed content from a third-party site, sandboxed to prevent that site from
// opening popups, etc, without preventing the embedded page from communicating back to its originating
// site, using the database APIs to store data, etc.
if (!tokens.contains_slow("allow-same-origin"sv))
output |= SandboxingFlagSet::SandboxedOrigin;
// - The sandboxed forms browsing context flag, unless tokens contains the allow-forms keyword.
if (!tokens.contains_slow("allow-forms"sv))
output |= SandboxingFlagSet::SandboxedForms;
// - The sandboxed pointer lock browsing context flag, unless tokens contains the allow-pointer-lock keyword.
if (!tokens.contains_slow("allow-pointer-lock"sv))
output |= SandboxingFlagSet::SandboxedPointerLock;
// - The sandboxed scripts browsing context flag, unless tokens contains the allow-scripts keyword.
// - The sandboxed automatic features browsing context flag, unless tokens contains the allow-scripts keyword
// (defined above).
// Spec Note: This flag is relaxed by the same keyword as scripts, because when scripts are enabled these features
// are trivially possible anyway, and it would be unfortunate to force authors to use script to do them
// when sandboxed rather than allowing them to use the declarative features.
if (!tokens.contains_slow("allow-scripts"sv)) {
output |= SandboxingFlagSet::SandboxedScripts;
output |= SandboxingFlagSet::SandboxedAutomaticFeatures;
}
// - The sandboxed document.domain browsing context flag.
output |= SandboxingFlagSet::SandboxedDocumentDomain;
// - The sandbox propagates to auxiliary browsing contexts flag, unless tokens contains the
// allow-popups-to-escape-sandbox keyword.
if (!tokens.contains_slow("allow-popups-to-escape-sandbox"sv))
output |= SandboxingFlagSet::SandboxPropagatesToAuxiliaryBrowsingContexts;
// - The sandboxed modals flag, unless tokens contains the allow-modals keyword.
if (!tokens.contains_slow("allow-modals"sv))
output |= SandboxingFlagSet::SandboxedModals;
// - The sandboxed orientation lock browsing context flag, unless tokens contains the allow-orientation-lock
// keyword.
if (!tokens.contains_slow("allow-orientation-lock"sv))
output |= SandboxingFlagSet::SandboxedOrientationLock;
// - The sandboxed presentation browsing context flag, unless tokens contains the allow-presentation keyword.
if (!tokens.contains_slow("allow-presentation"sv))
output |= SandboxingFlagSet::SandboxedPresentation;
// - The sandboxed downloads browsing context flag, unless tokens contains the allow-downloads keyword.
if (!tokens.contains_slow("allow-downloads"sv))
output |= SandboxingFlagSet::SandboxedDownloads;
// - The sandboxed custom protocols navigation browsing context flag, unless tokens contains either the
// allow-top-navigation-to-custom-protocols keyword, the allow-popups keyword, or the allow-top-navigation
// keyword.
if (!tokens.contains_slow("allow-top-navigation-to-custom-protocols"sv) && !tokens.contains_slow("allow-popups"sv) && !tokens.contains_slow("allow-top-navigation"sv))
output |= SandboxingFlagSet::SandboxedCustomProtocols;
return output;
}
}

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/EnumBits.h>
#include <AK/String.h>
#include <AK/Types.h>
namespace Web::HTML {
@ -35,4 +36,6 @@ enum class SandboxingFlagSet {
AK_ENUM_BITWISE_OPERATORS(SandboxingFlagSet);
inline bool is_empty(SandboxingFlagSet s) { return (to_underlying(s) & 0x1FFU) == 0; }
SandboxingFlagSet parse_a_sandboxing_directive(String const& input);
}

View file

@ -293,7 +293,9 @@ bool is_scripting_enabled(JS::Realm const& realm)
if (!document.page().is_scripting_enabled())
return false;
// FIXME: Either settings's global object is not a Window object, or settings's global object's associated Document's active sandboxing flag set does not have its sandboxed scripts browsing context flag set.
// Either settings's global object is not a Window object, or settings's global object's associated Document's active sandboxing flag set does not have its sandboxed scripts browsing context flag set.
if (has_flag(document.active_sandboxing_flag_set(), SandboxingFlagSet::SandboxedScripts))
return false;
return true;
}

View file

@ -0,0 +1,6 @@
<!doctype html>
<meta charset=utf-8>
<title>noscript parsing when sandbox disables scripting</title>
<iframe srcdoc="PASS" sandbox></iframe>
<iframe srcdoc="PASS" sandbox></iframe>
<iframe srcdoc="P<b>AS</b>S" sandbox></iframe>

View file

@ -0,0 +1,3 @@
<!DOCTYPE html>
<meta charset=utf-8>
<noscript>PASS</noscript>

View file

@ -0,0 +1,7 @@
<!doctype html>
<meta charset=utf-8>
<title>noscript parsing when sandbox disables scripting</title>
<link rel=match href=../../../../../expected/wpt-import//html/browsers/sandboxing/sandbox-parse-noscript-ref.html>
<iframe srcdoc="<noscript>PASS</noscript>" sandbox></iframe>
<iframe src="noscript-iframe.html" sandbox></iframe>
<iframe srcdoc="<noscript>P<b>AS</b>S</noscript>" sandbox></iframe>

View file

@ -320,3 +320,16 @@ Text/input/wpt-import/webaudio/the-audio-api/the-periodicwave-interface/periodic
; https://github.com/LadybirdBrowser/ladybird/issues/5333
Text/input/wpt-import/webaudio/the-audio-api/the-audionode-interface/audionode-disconnect.html
Text/input/wpt-import/webaudio/the-audio-api/the-audionode-interface/audionode-disconnect-audioparam.html
; Currently always timeout
Text/input/wpt-import/html/browsers/sandboxing/inner-iframe.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-inherited-from-initiator-response.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-javascript-window-open.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-disallow-scripts-via-unsandboxed-popup.tentative.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-document-open.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-inherited-from-required-csp.html
Text/input/wpt-import/html/browsers/sandboxing/sandbox-navigation-timing.tentative.html
; Not a ref test, but a subfile of the sandbox-parse-noscript ref test
Ref/input/wpt-import/html/browsers/sandboxing/noscript-iframe.html

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Fail
Fail CORS with sandboxed iframe

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass DOM access in sandbox='allow-same-origin' iframe is allowed

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Running script from sandbox='allow-scripts' iframe is allowed

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Fail
Fail window.open in sandbox iframe

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Access to sandbox iframe is disallowed

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Running script from sandbox iframe is disallowed

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Fail
Fail Using document.open() against a document from a different window must not mutate the other window's sandbox flags

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Inherit sandbox flags from the initiator's frame

View file

@ -0,0 +1,6 @@
Harness status: Timeout
Found 1 tests
1 Timeout
Timeout Inherit sandbox flags from the initiator's response

View file

@ -0,0 +1,6 @@
Harness status: Timeout
Found 1 tests
1 Timeout
Timeout setting sandbox attribute should not affect current document in iframe

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass iframe with sandbox should load with new execution context

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Fail
Fail window.open('about:srcdoc') from sandboxed srcdoc doesn't crash.

View file

@ -2,7 +2,6 @@ Harness status: OK
Found 2 tests
1 Pass
1 Fail
2 Pass
Pass iframe 'sandbox' ASCII case insensitive, allow-same-orİgin
Fail iframe 'sandbox' ASCII case insensitive, allow-ſcripts
Pass iframe 'sandbox' ASCII case insensitive, allow-ſcripts

View file

@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests
1 Fail
Fail When the scripting flag is disabled, a head start tag in "in head noscript" mode should be ignored
1 Pass
Pass When the scripting flag is disabled, a head start tag in "in head noscript" mode should be ignored

View file

@ -0,0 +1,98 @@
/**
* Create an absolute URL from `options` and defaulting unspecified properties to `window.location`.
* @param {Object} options - a `Location`-like object
* @param {string} options.hostname
* @param {string} options.subdomain - prepend subdomain to the hostname
* @param {string} options.port
* @param {string} options.path
* @param {string} options.query
* @param {string} options.hash
* @returns {string}
*/
function make_absolute_url(options) {
var loc = window.location;
var protocol = get(options, "protocol", loc.protocol);
if (protocol[protocol.length - 1] != ":") {
protocol += ":";
}
var hostname = get(options, "hostname", loc.hostname);
var subdomain = get(options, "subdomain");
if (subdomain) {
hostname = subdomain + "." + hostname;
}
var port = get(options, "port", loc.port)
var path = get(options, "path", loc.pathname);
var query = get(options, "query", loc.search);
var hash = get(options, "hash", loc.hash)
var url = protocol + "//" + hostname;
if (port) {
url += ":" + port;
}
if (path[0] != "/") {
url += "/";
}
url += path;
if (query) {
if (query[0] != "?") {
url += "?";
}
url += query;
}
if (hash) {
if (hash[0] != "#") {
url += "#";
}
url += hash;
}
return url;
}
/** @private */
function get(obj, name, default_val) {
if (obj.hasOwnProperty(name)) {
return obj[name];
}
return default_val;
}
/**
* Generate a new UUID.
* @returns {string}
*/
function token() {
var uuid = [to_hex(rand_int(32), 8),
to_hex(rand_int(16), 4),
to_hex(0x4000 | rand_int(12), 4),
to_hex(0x8000 | rand_int(14), 4),
to_hex(rand_int(48), 12)].join("-")
return uuid;
}
/** @private */
function rand_int(bits) {
if (bits < 1 || bits > 53) {
throw new TypeError();
} else {
if (bits >= 1 && bits <= 30) {
return 0 | ((1 << bits) * Math.random());
} else {
var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
var low = 0 | ((1 << 30) * Math.random());
return high + low;
}
}
}
/** @private */
function to_hex(x, length) {
var rv = x.toString(16);
while (rv.length < length) {
rv = "0" + rv;
}
return rv;
}

View file

@ -0,0 +1,14 @@
<!doctype html>
<html>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<iframe sandbox="allow-scripts" src="../resources/sandboxed-iframe.html"></iframe>
<script>
promise_test(async (t) => {
const message = await new Promise((resolve) => {
window.addEventListener('message', e => resolve(e.data));
});
assert_equals(message, 'PASS');
}, 'CORS with sandboxed iframe');
</script>
</html>

View file

@ -0,0 +1,34 @@
<!doctype html>
<html>
<script>
async function no_cors_should_be_rejected() {
let thrown = false;
try {
const resp = await fetch('top.txt');
} catch (e) {
thrown = true;
}
if (!thrown) {
throw Error('fetching "top.txt" should be rejected.');
}
}
async function null_origin_should_be_accepted() {
const url = 'top.txt?pipe=header(access-control-allow-origin,null)|' +
'header(cache-control,no-store)';
const resp = await fetch(url);
}
async function test() {
try {
await no_cors_should_be_rejected();
await null_origin_should_be_accepted();
parent.postMessage('PASS', '*');
} catch (e) {
parent.postMessage(e.message, '*');
}
}
test();
</script>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
top.calledFromIframe();
}
</script>
</head>
<body>
<div id="inner">foo</div>
</body>
</html>

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>DOM access in sandbox="allow-same-origin" iframe</title>
<link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
<link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<h1>DOM access in sandbox="allow-same-origin" iframe</h1>
<script type="text/javascript">
var t = async_test("DOM access in sandbox='allow-same-origin' iframe is allowed")
var called = 0;
function calledFromIframe() {
called++;
}
function loaded() {
assert_equals(document.getElementById('sandboxedframe').contentWindow.document.getElementById('inner').innerHTML, 'foo');
assert_equals(called, 0);
t.done();
}
</script>
<iframe src="../../../html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox="allow-same-origin" id="sandboxedframe" onload="loaded();"></iframe>
<div id="log"></div>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Script execution in sandbox="allow-scripts" iframe</title>
<link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
<link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<h1>Script execution in sandbox="allow-scripts" iframe</h1>
<script type="text/javascript">
var t = async_test("Running script from sandbox='allow-scripts' iframe is allowed")
var called = 0;
function calledFromIframe() {
called++;
}
function loaded() {
assert_equals(called, 1);
t.done();
}
</script>
<iframe src="../../../html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox="allow-scripts allow-same-origin" id="sandboxedframe" onload="loaded();"></iframe>
<div id="log"></div>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>window.open in sandbox iframe</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../common/utils.js"></script>
<body>
<script>
setup({single_test: true});
// check that the popup's URL is not loaded
const uuid = token();
async function assert_popup_not_loaded() {
const response = await fetch(`/fetch/api/resources/stash-take.py?key=${uuid}`);
assert_equals(await response.json(), null); // is "loaded" if it loads
}
// check for message from the iframe
window.onmessage = e => {
assert_equals(e.data, 'null', 'return value of window.open (stringified)');
step_timeout(async () => {
await assert_popup_not_loaded();
done();
}, 1000);
};
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts';
iframe.srcdoc = `
<script>
let result;
try {
result = window.open('/fetch/api/resources/stash-put.py?key=${uuid}&value=loaded', '_blank');
} catch(ex) {
result = ex;
}
parent.postMessage(String(result), '*');
<\/script>
`;
document.body.appendChild(iframe);
</script>

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Access to sandbox iframe</title>
<link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#sandboxed-origin-browsing-context-flag">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#integration-with-idl">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<h1>Access to sandbox iframe</h1>
<script type="text/javascript">
var t = async_test("Access to sandbox iframe is disallowed")
var called = 0;
function calledFromIframe() {
called++;
}
function loaded() {
t.step(() => {
assert_throws_dom("SecurityError", () => {
document.getElementById('sandboxedframe').contentWindow.document;
});
assert_equals(called, 0);
t.done();
});
}
</script>
<iframe src="../../../html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox id="sandboxedframe" onload="loaded();"></iframe>
</body>
<div id="log"></div>
</html>

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<body>
<script>
async_test(t => {
let i = document.createElement('iframe');
i.sandbox = "allow-same-origin allow-popups allow-popups-to-escape-sandbox";
i.srcdoc = `<a target='_blank' rel='opener'
href="javascript:window.opener.top.postMessage('FAIL', '*');">Click me!</a>
<a target='_blank' rel='opener'
href="./resources/post-done-to-opener.html">Click me next!</a>`;
i.onload = _ => {
// Since the frame is sandboxed, but allow-same-origin, we can reach into it to grab the
// anchor element to click. We'll click the `javascript:` URL first, then pop up a new
// window that posts `DONE`.
//
// TODO(mkwst): This feels like a race, but it's one that we consistently win when I'm
// running the test locally 10,000 times. Good enough!™
i.contentDocument.body.querySelectorAll('a')[0].click();
i.contentDocument.body.querySelectorAll('a')[1].click();
};
document.body.appendChild(i);
window.addEventListener('message', t.step_func(e => {
assert_not_equals(e.data, "FAIL");
if (e.data == "DONE")
t.done();
}));
}, "Sandboxed => unsandboxed popup");
</script>
</body>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Script execution in sandbox iframe</title>
<link rel="author" title="Kinuko Yasuda" href="mailto:kinuko@chromium.org">
<link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<h1>Script execution in sandbox iframe</h1>
<script type="text/javascript">
var t = async_test("Running script from sandbox iframe is disallowed")
var called = 0;
function calledFromIframe() {
called++;
}
function loaded() {
assert_equals(called, 0);
t.done();
}
</script>
<iframe src="../../../html/browsers/sandboxing/inner-iframe.html" style="visibility:hidden;display:none" sandbox id="sandboxedframe" onload="loaded();"></iframe>
<div id="log"></div>
</body>
</html>

View file

@ -0,0 +1,8 @@
<!doctype html>
<meta charset=utf-8>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../html/browsers/sandboxing/sandbox-document-open-mutation.window.js"></script>

View file

@ -0,0 +1,37 @@
// Return whether the current context is sandboxed or not. The implementation do
// not matter much, but might have to change over time depending on what side
// effect sandbox flag have. Feel free to update as needed.
const is_sandboxed = () => {
try {
document.domain = document.domain;
return "not sandboxed";
} catch (error) {
return "sandboxed";
}
};
promise_test(async test => {
const message = new Promise(r => window.addEventListener("message", r));
const iframe_unsandboxed = document.createElement("iframe");
document.body.appendChild(iframe_unsandboxed);
const iframe_sandboxed = document.createElement("iframe");
iframe_sandboxed.sandbox = "allow-same-origin allow-scripts";
document.body.appendChild(iframe_sandboxed);
iframe_sandboxed.srcdoc = `
<script>
parent.frames[0].document.write(\`
<script>
const is_sandboxed = ${is_sandboxed};
window.parent.postMessage(is_sandboxed(), '*');
</scr\`+\`ipt>
\`);
parent.frames[0].document.close();
</scr`+`ipt>
`;
assert_equals((await message).data, "not sandboxed");
}, "Using document.open() against a document from a different window must not" +
" mutate the other window's sandbox flags");

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>
Check sandbox-flags aren't lost after using document.open().
</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<body>
<script>
promise_test(async test => {
let message = new Promise(resolve =>
window.addEventListener("message", event => resolve(event.data))
);
let iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.setAttribute("src", "./resources/document-open.html")
document.body.appendChild(iframe);
assert_equals(await message, "document-domain-is-disallowed");
}, "document.open()");
promise_test(async test => {
let iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.setAttribute("src", "/common/blank.html");
let loaded = new Promise(resolve => iframe.onload = resolve);
document.body.appendChild(iframe);
await loaded;
let message = new Promise(resolve =>
window.addEventListener("message", event => resolve(event.data))
);
iframe.contentDocument.write(`
<script>
try {
document.domain = document.domain;
parent.postMessage('document-domain-is-allowed', '*');
} catch (error) {
parent.postMessage('document-domain-is-disallowed', '*');
}
</sc`+`ript>
`);
assert_equals(await message, "document-domain-is-disallowed");
}, "other_document.open()");
</script>
</body>
</html>

View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Inherit sandbox flags from the initiator's frame</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<body>
<script>
// Check sandbox flags are properly inherited when a document initiate a
// navigation inside another frame that it doesn't own directly.
// This check the sandbox flags defined by the frame. See also the other test
// about sandbox flags defined by the response (e.g. CSP sandbox):
// => sandbox-inherited-from-initiators-response.html
// Return a promise, resolving when |element| triggers |event_name| event.
let future = (element, event_name) => {
return new Promise(resolve => {
element.addEventListener(event_name, event => resolve(event))
});
};
promise_test(async test => {
const iframe_1 = document.createElement("iframe");
const iframe_2 = document.createElement("iframe");
iframe_1.id = "iframe_1";
iframe_2.id = "iframe_2";
const iframe_1_script = encodeURI(`
<script>
try {
document.domain = document.domain;
parent.postMessage("not sandboxed", "*");
} catch (exception) {
parent.postMessage("sandboxed", "*");
}
</scr`+`ipt>
`);
const iframe_2_script = `
<script>
const iframe_1 = parent.document.querySelector("#iframe_1");
iframe_1.src = "data:text/html,${iframe_1_script}";
</scr`+`ipt>
`;
iframe_2.sandbox = "allow-scripts allow-same-origin";
iframe_2.srcdoc = iframe_2_script;
// Insert |iframe_1|. It will load the initial empty document, with no sandbox
// flags.
const iframe_1_load_1 = future(iframe_1, "load");
document.body.appendChild(iframe_1);
await iframe_1_load_1;
// Insert |iframe_2|. It will load with sandbox flags. It will make |iframe_1|
// to navigate toward a data-url, which should inherit the sandbox flags.
const iframe_1_reply = future(window, "message");
document.body.appendChild(iframe_2);
const result = await iframe_1_reply;
assert_equals("sandboxed", result.data);
})
</script>

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Inherit sandbox flags from the initiator's response</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<body>
<script>
// Check sandbox flags are properly inherited when a document initiate a
// navigation inside another frame that it doesn't own directly.
// This check the sandbox flags defined by the response (e.g. CSP sandbox). See
// also the other test about sandbox flags inherited from the frame.
// => sandbox-inherited-from-initiators-frame.html
// Return a promise, resolving when |element| triggers |event_name| event.
let future = (element, event_name) => {
return new Promise(resolve => {
element.addEventListener(event_name, event => resolve(event))
});
};
promise_test(async test => {
const iframe_1 = document.createElement("iframe");
const iframe_2 = document.createElement("iframe");
iframe_1.id = "iframe_1";
iframe_2.id = "iframe_2";
iframe_2.src =
"./resources/sandbox-inherited-from-initiator-response-helper.html";
// Insert |iframe_1|. It will load the initial empty document, with no sandbox
// flags.
const iframe_1_load_1 = future(iframe_1, "load");
document.body.appendChild(iframe_1);
await iframe_1_load_1;
// Insert |iframe_2|. It will load with sandbox flags. It will make |iframe_1|
// to navigate toward a data-url, which should inherit the sandbox flags.
const iframe_1_reply = future(window, "message");
document.body.appendChild(iframe_2);
const result = await iframe_1_reply;
assert_equals("sandboxed", result.data);
})
</script>

View file

@ -0,0 +1,154 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Inherit sandbox from CSP embedded enforcement</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../common/get-host-info.sub.js"></script>
<body>
<script>
// Check sandbox flags are properly defined when its parent requires them and
// the child allows it.
const same_origin = get_host_info().HTTP_ORIGIN;
const cross_origin = get_host_info().HTTP_REMOTE_ORIGIN;
const check_sandbox_url =
"/html/browsers/sandboxing/resources/check-sandbox-flags.html?pipe=";
const allow_csp_from_star = "|header(Allow-CSP-From,*)";
// Return a promise, resolving when |element| triggers |event_name| event.
const future = (element, event_name, source) => {
return new Promise(resolve => {
element.addEventListener(event_name, event => {
if (!source || source.contentWindow == event.source)
resolve(event)
})
});
};
const check_sandbox_script = `
<script>
try {
document.domain = document.domain;
parent.postMessage("document-domain-is-allowed", "*");
} catch (exception) {
parent.postMessage("document-domain-is-disallowed", "*");
}
</scr`+`ipt>
`;
const sandbox_policy = "sandbox allow-scripts allow-same-origin";
// Test using the modern async/await primitives are easier to read/write.
// However they run sequentially, contrary to async_test. This is the parallel
// version, to avoid timing out.
let promise_test_parallel = (promise, description) => {
async_test(test => {
promise(test)
.then(() => {test.done();})
.catch(test.step_func(error => { throw error; }));
}, description);
};
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.csp = sandbox_policy;
// The <iframe> immediately hosts the initial empty document after being
// appended into the DOM. It will, as long as its 'src' isn't loaded. That's
// why a page do not load is being used.
iframe.src = "/fetch/api/resources/infinite-slow-response.py";
document.body.appendChild(iframe);
const iframe_reply = future(window, "message", iframe);
iframe.contentDocument.write(check_sandbox_script);
const result = await iframe_reply;
iframe.remove();
assert_equals(result.data, "document-domain-is-disallowed");
}, "initial empty document");
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.src = "data:text/html,dummy";
const iframe_load_1 = future(iframe, "load");
document.body.appendChild(iframe);
await iframe_load_1;
const iframe_load_2 = future(iframe, "load");
iframe.csp = sandbox_policy;
iframe.src = "about:blank";
await iframe_load_2;
const iframe_reply = future(window, "message", iframe);
iframe.contentDocument.write(check_sandbox_script);
const result = await iframe_reply;
assert_equals(result.data, "document-domain-is-disallowed");
}, "about:blank");
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.csp = sandbox_policy;
iframe.src =
`data:text/html,${encodeURI(check_sandbox_script)}`;
const iframe_reply = future(window, "message", iframe);
document.body.appendChild(iframe);
const result = await iframe_reply;
assert_equals(result.data, "document-domain-is-disallowed");
}, "data-url");
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.csp = sandbox_policy;
iframe.srcdoc = check_sandbox_script;
const iframe_reply = future(window, "message", iframe);
document.body.appendChild(iframe);
const result = await iframe_reply;
assert_equals(result.data, "document-domain-is-disallowed");
}, "srcdoc");
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.csp = sandbox_policy;
const blob = new Blob([check_sandbox_script], { type: "text/html" });
iframe.src = URL.createObjectURL(blob);
const iframe_reply = future(window, "message", iframe);
document.body.appendChild(iframe);
const result = await iframe_reply;
assert_equals(result.data, "document-domain-is-disallowed");
}, "blob URL");
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.csp = sandbox_policy;
iframe.src = same_origin + check_sandbox_url + allow_csp_from_star;
const iframe_reply = future(window, "message", iframe);
document.body.appendChild(iframe);
const result = await iframe_reply;
assert_equals(result.data, "document-domain-is-disallowed");
}, "same-origin");
promise_test_parallel(async test => {
const iframe = document.createElement("iframe");
iframe.csp = sandbox_policy;
iframe.src = cross_origin + check_sandbox_url + allow_csp_from_star;
const iframe_reply = future(window, "message", iframe);
document.body.appendChild(iframe);
const result = await iframe_reply;
assert_equals(result.data, "document-domain-is-disallowed");
}, "cross-origin");
</script>

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>
Check sandbox-flags inheritance in case of javascript window reuse.
</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<body>
<script>
promise_test(async test => {
let message = new Promise(resolve =>
window.addEventListener("message", event => resolve(event.data))
);
// Create an initial empty document in the iframe, sandboxed. It will attempt
// to load a slow page, but won't have time.
let iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.src = "/fetch/api/resources/infinite-slow-response.py";
document.body.appendChild(iframe);
// Remove sandbox flags. This should apply to documents committed from
// navigations started after this instruction.
iframe.removeAttribute("sandbox");
iframe.src = "./resources/check-sandbox-flags.html";
// The window is reused, but the new sandbox flags should be used.
assert_equals(await message, "document-domain-is-allowed");
});
</script>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>window.open in sandbox iframe</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../common/utils.js"></script>
<body>
<script>
promise_test(async test => {
let message = new Promise(resolve => {
window.addEventListener("message", event => resolve(event.data));
});
let iframe = document.createElement("iframe");
iframe.sandbox = "allow-scripts allow-popups allow-same-origin";
iframe.src = "./resources/sandbox-javascript-window-open.html";
document.body.appendChild(iframe);
assert_equals(await message, "disallow-document-domain");
});
</script>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Sandbox Navigation Timing</title>
<script src=../../../resources/testharness.js></script>
<script src=../../../resources/testharnessreport.js></script>
<html></html>
<script>
const sandboxUrl = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1) + 'sandbox-navigation-timing-iframe.tentative.html';
async_test(t => {
const iframe = document.createElement('iframe');
iframe.src = sandboxUrl;
document.body.appendChild(iframe); // Navigation starts; value of sandbox flags locked on.
// This should not affect the sandbox value used for both about:blank document
// and the final document in iframe.
iframe.sandbox = 'allow-scripts';
const iframeAboutBlankDocument = iframe.contentDocument;
iframe.onload = t.step_func(() => {
const iframeAboutBlankContents = iframeAboutBlankDocument.querySelectorAll('body');
assert_equals(iframeAboutBlankContents[0].tagName, "BODY",
"about:blank document's contents should still be accessible");
iframe.contentWindow.postMessage("is iframe sandboxed?", "*");
});
window.onmessage = t.step_func_done(e => {
assert_equals(e.data.result, 'iframe not sandboxed');
});
}, 'setting sandbox attribute should not affect current document in iframe');
</script>

View file

@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<title>Reuse of iframe about:blank document execution context</title>
<link rel="author" title="Dan Clark" href="mailto:daniec@microsoft.com">
<link rel="help" href="http://www.w3.org/html/wg/drafts/html/master/browsers.html#sandboxing">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
</head>
<body>
<h1>Reuse of iframe about:blank document execution context in sandbox="allow-scripts" iframe</h1>
<script type="text/javascript">
async_test(t => {
let iframe = document.createElement("iframe");
document.body.appendChild(iframe);
let iframeAboutBlankDocument = iframe.contentDocument;
assert_equals(iframeAboutBlankDocument.URL, "about:blank");
iframe.sandbox = "allow-scripts";
iframe.src = './sandbox-new-execution-context-iframe.html';
iframe.onload = t.step_func_done(() => {
assert_equals(iframe.contentDocument, null,
"New document in sandboxed iframe should have opaque origin");
assert_equals(Object.getPrototypeOf(iframeAboutBlankDocument).changeFromSandboxedIframe, undefined,
"Sandboxed iframe contents should not have been able to mess with type system of about:blank document");
let iframeAboutBlankContents = iframeAboutBlankDocument.querySelectorAll('body');
assert_equals(iframeAboutBlankContents[0].tagName, "BODY",
"about:blank document's contents should still be accessible");
});
},"iframe with sandbox should load with new execution context");
</script>
<div id="log"></div>
</body>
</html>

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>window.open("about:srcdoc") from a sandboxed iframe</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<body>
<script>
// Check what happens when executing window.open("about:srcdoc") from a
// sandboxed iframe. Srcdoc can't be loaded in the main frame. It should
// result in an error page. The error page should be cross-origin with the
// opener.
//
// This test covers an interesting edge case. A main frame should inherit
// sandbox flags. However the document loaded is an internal error page. This
// might trigger some assertions, especially if the implementation wrongly
// applies the sandbox flags of the opener to the internal error page document.
//
// This test is mainly a coverage test. It passes if it doesn't crash.
async_test(test => {
let iframe = document.createElement("iframe");
iframe.sandbox = "allow-scripts allow-popups allow-same-origin";
iframe.srcdoc = `
<script>
let w = window.open();
onunload = () => w.close();
let notify = () => {
try {
w.origin; // Will fail after navigating to about:srcdoc.
parent.postMessage("pending", "*");
} catch (e) {
parent.postMessage("done", "*");
};
};
addEventListener("message", notify);
notify();
w.location = "about:srcdoc"; // Error page.
</scr`+`ipt>
`;
let closed = false;
addEventListener("message", event => {
closed = (event.data === "done");
iframe.contentWindow.postMessage("ping","*");
});
document.body.appendChild(iframe);
test.step_wait_func_done(()=>closed);
}, "window.open('about:srcdoc') from sandboxed srcdoc doesn't crash.");
</script>