Implement policy: 'document-stream-insertion'
This CL adds the actual implementation for the experimental policy
'document-stream-insertion'. The policy is used to block usages of
specific APIs mentioned section "dynamic markup insertion" of the HTML
spec. This essentially includes document.{close, open, write, writeln}.
With the current CL, the calls to banned API lead to a DOMException.
The feature itself was introduced in a previous CL:
https://chromium-review.googlesource.com/c/chromium/src/+/1053349
Bug: 841605
Change-Id: I1a764bc7545a0d26a29d217027cf43e561d8dfbd
Reviewed-on: https://chromium-review.googlesource.com/1058138
Commit-Queue: Ehsan Karamad <ekaramad@chromium.org>
Reviewed-by: Ian Clelland <iclelland@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/master@{#561275}
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/document-stream-insertion.tentative.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/document-stream-insertion.tentative.html
new file mode 100644
index 0000000..12b844c
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/document-stream-insertion.tentative.html
@@ -0,0 +1,82 @@
+<!doctype html>
+<title>'document-stream-insertion' tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/feature-policy/experimental-features/resources/common.js"></script>
+<style>
+html, body {
+ height: 100%;
+ width: 100%;
+}
+</style>
+<iframe></iframe>
+<script>
+ "use strict";
+
+ let iframeElement = document.querySelector("iframe");
+ let url = url_base + "document-stream-insertion.html";
+
+ let text_to_write = "<div>FOO<\/div>";
+ let test_cases = [{
+ api: "open",
+ query: "body",
+ expected_value_enabled: false,
+ },
+ {
+ api: "close"
+ },
+ {
+ api: "write",
+ args: text_to_write,
+ query: "div",
+ expected_value_enabled: "FOO"
+ },
+ {
+ api: "writeln",
+ args: text_to_write,
+ query: "div",
+ expected_value_enabled: "FOO"
+ }];
+
+ // The feature 'document-stream-insertion' is enabled by default and when it
+ // is enabled, all dynamic markup insertion API work as intended.
+ test_cases.forEach((tc) => {
+ promise_test(async() => {
+ await loadUrlInIframe(iframeElement, url);
+ await sendMessageAndGetResponse(iframeElement.contentWindow, tc).then((response) => {
+ assert_false(
+ response.did_throw_exception,
+ `When feature is disabled, invoking 'document.${tc.api}' should not` +
+ " throw an exception.");
+ if (tc.query) {
+ assert_equals(
+ response.value,
+ tc.expected_value_enabled,
+ `The added script tag by 'document.${tc.api}' must have run.`);
+ }
+ });
+ }, `Verify 'document.${tc.api}' is not normally blocked.` );
+ });
+
+
+ // Disabling 'document-stream-insertion' throws exception on the included API.
+ test_cases.forEach((tc) => {
+ promise_test(async() => {
+ setFeatureState(iframeElement, "document-stream-insertion", "'none'");
+ await loadUrlInIframe(iframeElement, url);
+ await sendMessageAndGetResponse(iframeElement.contentWindow, tc).then((response) => {
+ assert_true(
+ response.did_throw_exception,
+ `When feature is enabled, invoking 'document.${tc.api}' should ` +
+ " throw an exception.");
+ if (tc.query) {
+ assert_not_equals(
+ response.value,
+ tc.expected_value_enabled,
+ `The added script tag by 'document.${tc.api}' must not have run.`);
+ }
+ });
+ }, `Verify 'document.${tc.api}' is blocked when the feature is disabled.` );
+ });
+
+</script>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/common.js b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/common.js
new file mode 100644
index 0000000..08d3aef
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/common.js
@@ -0,0 +1,34 @@
+const url_base = "/feature-policy/experimental-features/resources/";
+window.messageResponseCallback = null;
+
+function setFeatureState(iframe, feature, origins) {
+ iframe.setAttribute("allow", `${feature} ${origins};`);
+}
+
+// Returns a promise which is resolved when the <iframe> is navigated to |url|
+// and "load" handler has been called.
+function loadUrlInIframe(iframe, url) {
+ return new Promise((resolve) => {
+ iframe.addEventListener("load", resolve);
+ iframe.src = url;
+ });
+}
+
+// Posts |message| to |target| and resolves the promise with the response coming
+// back from |target|.
+function sendMessageAndGetResponse(target, message) {
+ return new Promise((resolve) => {
+ window.messageResponseCallback = resolve;
+ target.postMessage(message, "*");
+ });
+}
+
+
+function onMessage(e) {
+ if (window.messageResponseCallback) {
+ window.messageResponseCallback(e.data);
+ window.messageResponseCallback = null;
+ }
+}
+
+window.addEventListener("message", onMessage);
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/document-stream-insertion.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/document-stream-insertion.html
new file mode 100644
index 0000000..633fa85
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/document-stream-insertion.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<style>
+#spacer {
+ width: 200%;
+ height: 200%;
+}
+</style>
+<body>
+<script>
+ window.addEventListener("message", onMessageReceived);
+
+ function test(api, args) {
+ let did_throw = false;
+ try {
+ document[api](args);
+ } catch(e) {
+ did_throw = true;
+ }
+ return did_throw;
+ }
+
+ function onMessageReceived(e) {
+ let msg = e.data;
+
+ msg.did_throw_exception = test(msg.api, msg.args);
+ if (msg.query) {
+ let el = document.querySelector(msg.query);
+ msg.value = el ? el.innerHTML : false;
+ }
+ ackMessage(msg, e.source);
+ }
+
+ function ackMessage(msg, source) {
+ source.postMessage(msg, "*");
+ }
+</script>
+</body>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/vertical-scroll.js b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/vertical-scroll.js
index bbae658..88835cc 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/vertical-scroll.js
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/resources/vertical-scroll.js
@@ -1,6 +1,3 @@
-const url_base = "/feature-policy/experimental-features/resources/";
-window.messageResponseCallback = null;
-
function rectMaxY(rect) {
return rect.height + rect.y;
}
@@ -23,33 +20,6 @@
rect2.y < rectMaxY(rect1);
}
-// Returns a promise which is resolved when the <iframe> is navigated to |url|
-// and "load" handler has been called.
-function loadUrlInIframe(iframe, url) {
- return new Promise((resolve) => {
- iframe.addEventListener("load", resolve);
- iframe.src = url;
- });
-}
-
-// Posts |message| to |target| and resolves the promise with the response coming
-// back from |target|.
-function sendMessageAndGetResponse(target, message) {
- return new Promise((resolve) => {
- window.messageResponseCallback = resolve;
- target.postMessage(message, "*");
- });
-}
-
function rectToString(rect) {
return `Location: (${rect.x}, ${rect.y}) Size: (${rect.width}, ${rect.height})`;
}
-
-function onMessage(e) {
- if (window.messageResponseCallback) {
- window.messageResponseCallback(e.data);
- window.messageResponseCallback = null;
- }
-}
-
-window.addEventListener("message", onMessage);
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html
index e9c7cba..689685a 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-scrollintoview.tentative.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
+<script src="/feature-policy/experimental-features/resources/common.js"></script>
<script src="/feature-policy/experimental-features/resources/vertical-scroll.js"></script>
<style>
html, body {
@@ -73,7 +74,7 @@
// of disabled frames (does not propagate to main frame).
promise_test(async() => {
window.scrollTo(0, 0);
- iframeElement.allow = "vertical-scroll 'none';";
+ setFeatureState(iframeElement, "vertical-scroll", "'none'");
await loadUrlInIframe(iframeElement, url);
await sendMessageAndGetResponse(
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-touch-action-manual.tentative.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-touch-action-manual.tentative.html
index 761c0d7..ed6bff1 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-touch-action-manual.tentative.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/experimental-features/vertical-scroll-touch-action-manual.tentative.html
@@ -2,6 +2,7 @@
<title>vertical-scroll test for touch-action</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
+<script src="/feature-policy/experimental-features/resources/common.js"></script>
<script src="/feature-policy/experimental-features/resources/vertical-scroll.js"></script>
<style>
html, body {
@@ -81,7 +82,7 @@
window.scrollTo(0, 0);
// Disallow vertical scroll and reload the <iframe>.
- iframeElement.setAttribute("allow", "vertical-scroll 'none';");
+ setFeatureState(iframeElement, "vertical-scroll", "'none'");
await loadUrlInIframe(iframeElement, url);
// Apply the scroll gesture. Main frame should scroll vertically.
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 3ec6680..76cf6d8 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -2953,6 +2953,9 @@
return;
}
+ if (!AllowedToUseDynamicMarkUpInsertion("open", exception_state))
+ return;
+
if (entered_document) {
if (!GetSecurityOrigin()->IsSameSchemeHostPort(
entered_document->GetSecurityOrigin())) {
@@ -3222,6 +3225,9 @@
return;
}
+ if (!AllowedToUseDynamicMarkUpInsertion("close", exception_state))
+ return;
+
close();
}
@@ -3717,6 +3723,9 @@
return;
}
+ if (!AllowedToUseDynamicMarkUpInsertion("write", exception_state))
+ return;
+
StringBuilder builder;
for (const String& string : text)
builder.Append(string);
@@ -3735,6 +3744,9 @@
return;
}
+ if (!AllowedToUseDynamicMarkUpInsertion("writeln", exception_state))
+ return;
+
StringBuilder builder;
for (const String& string : text)
builder.Append(string);
@@ -6021,6 +6033,32 @@
parent_feature_policy);
}
+bool Document::AllowedToUseDynamicMarkUpInsertion(
+ const char* api_name,
+ ExceptionState& exception_state) {
+ if (!IsSupportedInFeaturePolicy(
+ mojom::FeaturePolicyFeature::kDocumentStreamInsertion)) {
+ return true;
+ }
+ if (!frame_ || frame_->IsFeatureEnabled(
+ mojom::FeaturePolicyFeature::kDocumentStreamInsertion)) {
+ return true;
+ }
+
+ // TODO(ekaramad): Throwing an exception seems an ideal resolution to mishaps
+ // in using the API against the policy. But this cannot be applied to cross-
+ // origin as there are security risks involved. We should perhaps unload the
+ // whole frame instead of throwing.
+ exception_state.ThrowDOMException(
+ kNotAllowedError,
+ String::Format(
+ "The use of method '%s' has been blocked by feature policy. The "
+ "feature "
+ "'document-stream-insertion' is disabled in this document.",
+ api_name));
+ return false;
+}
+
ukm::UkmRecorder* Document::UkmRecorder() {
if (ukm_recorder_)
return ukm_recorder_.get();
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index 53e2acf..13bc7ba 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -1547,6 +1547,11 @@
// the LocalFrameClient.
void ApplyFeaturePolicy(const ParsedFeaturePolicy& declared_policy);
+ // Returns true if use of |method_name| for markup insertion is allowed by
+ // feature policy; otherwise returns false and throws a DOM exception.
+ bool AllowedToUseDynamicMarkUpInsertion(const char* method_name,
+ ExceptionState&);
+
void SetFreezingInProgress(bool is_freezing_in_progress) {
is_freezing_in_progress_ = is_freezing_in_progress;
};