Bug 1540221 - Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean, r=aosmond
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 16 Apr 2019 06:58:29 +0000
changeset 469629 3fd42eb67a3f
parent 469628 eb8d178cd72a
child 469630 cfd46a25af71
push id35878
push userapavel@mozilla.com
push dateTue, 16 Apr 2019 15:43:40 +0000
treeherdermozilla-central@258af4e91151 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaosmond
bugs1540221
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1540221 - Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean, r=aosmond Differential Revision: https://phabricator.services.mozilla.com/D25773
dom/canvas/CanvasRenderingContext2D.cpp
dom/canvas/CanvasUtils.cpp
dom/canvas/CanvasUtils.h
dom/canvas/ImageBitmap.cpp
layout/base/nsLayoutUtils.cpp
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.redirect.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.redirect.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.fillStyle.sub.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.redirect.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.redirect.html.ini
testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-origin.sub.html
testing/web-platform/tests/common/canvas-tests.js
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.fillStyle.sub.html
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -1926,17 +1926,21 @@ void CanvasRenderingContext2D::SetStyleF
   }
 
   if (aValue.IsCanvasGradient()) {
     SetStyleFromGradient(aValue.GetAsCanvasGradient(), aWhichStyle);
     return;
   }
 
   if (aValue.IsCanvasPattern()) {
-    SetStyleFromPattern(aValue.GetAsCanvasPattern(), aWhichStyle);
+    CanvasPattern& pattern = aValue.GetAsCanvasPattern();
+    SetStyleFromPattern(pattern, aWhichStyle);
+    if (pattern.mForceWriteOnly) {
+      SetWriteOnly();
+    }
     return;
   }
 
   MOZ_ASSERT_UNREACHABLE("Invalid union value");
 }
 
 void CanvasRenderingContext2D::SetFillRule(const nsAString& aString) {
   FillRule rule;
@@ -2094,23 +2098,24 @@ already_AddRefed<CanvasPattern> CanvasRe
   }
 
   // The canvas spec says that createPattern should use the first frame
   // of animated images
   nsLayoutUtils::SurfaceFromElementResult res =
       nsLayoutUtils::SurfaceFromElement(
           element, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE, mTarget);
 
-  if (!res.GetSourceSurface()) {
+  RefPtr<SourceSurface> surface = res.GetSourceSurface();
+  if (!surface) {
     return nullptr;
   }
 
   RefPtr<CanvasPattern> pat =
-      new CanvasPattern(this, res.GetSourceSurface(), repeatMode,
-                        res.mPrincipal, res.mIsWriteOnly, res.mCORSUsed);
+      new CanvasPattern(this, surface, repeatMode, res.mPrincipal,
+                        res.mIsWriteOnly, res.mCORSUsed);
   return pat.forget();
 }
 
 //
 // shadows
 //
 void CanvasRenderingContext2D::SetShadowColor(const nsAString& aShadowColor) {
   nscolor color;
@@ -4225,18 +4230,18 @@ CanvasRenderingContext2D::CachedSurfaceF
 
   int32_t corsmode = imgIRequest::CORS_NONE;
   if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) {
     res.mCORSUsed = corsmode != imgIRequest::CORS_NONE;
   }
 
   res.mSize = res.mSourceSurface->GetSize();
   res.mPrincipal = principal.forget();
-  res.mIsWriteOnly = false;
   res.mImageRequest = imgRequest.forget();
+  res.mIsWriteOnly = CheckWriteOnlySecurity(res.mCORSUsed, res.mPrincipal);
 
   return res;
 }
 
 // drawImage(in HTMLImageElement image, in float dx, in float dy);
 //   -- render image from 0,0 at dx,dy top-left coords
 // drawImage(in HTMLImageElement image, in float dx, in float dy, in float dw,
 //           in float dh);
--- a/dom/canvas/CanvasUtils.cpp
+++ b/dom/canvas/CanvasUtils.cpp
@@ -296,10 +296,30 @@ bool CoerceDouble(const JS::Value& v, do
   return true;
 }
 
 bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* /* unused */) {
   return nsContentUtils::CallerHasPermission(aCx,
                                              nsGkAtoms::all_urlsPermission);
 }
 
+bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal) {
+  if (!aPrincipal) {
+    return true;
+  }
+
+  if (!aCORSUsed) {
+    nsIGlobalObject* incumbentSettingsObject = dom::GetIncumbentGlobal();
+    if (NS_WARN_IF(!incumbentSettingsObject)) {
+      return true;
+    }
+
+    nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
+    if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
 }  // namespace CanvasUtils
 }  // namespace mozilla
--- a/dom/canvas/CanvasUtils.h
+++ b/dom/canvas/CanvasUtils.h
@@ -6,16 +6,17 @@
 #ifndef _CANVASUTILS_H_
 #define _CANVASUTILS_H_
 
 #include "CanvasRenderingContextHelper.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/dom/ToJSValue.h"
 #include "jsapi.h"
 #include "mozilla/FloatingPoint.h"
+#include "nsLayoutUtils.h"
 
 class nsIPrincipal;
 
 namespace mozilla {
 
 namespace dom {
 class HTMLCanvasElement;
 }  // namespace dom
@@ -168,12 +169,16 @@ void DashArrayToJSVal(nsTArray<T>& dashe
     return;
   }
   JS::Rooted<JS::Value> val(cx);
   if (!mozilla::dom::ToJSValue(cx, dashes, retval)) {
     rv.Throw(NS_ERROR_OUT_OF_MEMORY);
   }
 }
 
+// returns true if write-only mode must used for this principal based on
+// the incumbent global.
+bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal);
+
 }  // namespace CanvasUtils
 }  // namespace mozilla
 
 #endif /* _CANVASUTILS_H_ */
--- a/dom/canvas/ImageBitmap.cpp
+++ b/dom/canvas/ImageBitmap.cpp
@@ -407,43 +407,16 @@ class CreateImageFromRawDataInMainThread
   uint8_t* mBuffer;
   uint32_t mBufferLength;
   uint32_t mStride;
   gfx::SurfaceFormat mFormat;
   gfx::IntSize mSize;
   const Maybe<IntRect>& mCropRect;
 };
 
-static bool CheckSecurityForElements(bool aIsWriteOnly, bool aCORSUsed,
-                                     nsIPrincipal* aPrincipal) {
-  if (aIsWriteOnly || !aPrincipal) {
-    return false;
-  }
-
-  if (!aCORSUsed) {
-    nsIGlobalObject* incumbentSettingsObject = GetIncumbentGlobal();
-    if (NS_WARN_IF(!incumbentSettingsObject)) {
-      return false;
-    }
-
-    nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
-    if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
-      return false;
-    }
-  }
-
-  return true;
-}
-
-static bool CheckSecurityForElements(
-    const nsLayoutUtils::SurfaceFromElementResult& aRes) {
-  return CheckSecurityForElements(aRes.mIsWriteOnly, aRes.mCORSUsed,
-                                  aRes.mPrincipal);
-}
-
 /*
  * A wrapper to the nsLayoutUtils::SurfaceFromElement() function followed by the
  * security checking.
  */
 template <class ElementType>
 static already_AddRefed<SourceSurface> GetSurfaceFromElement(
     nsIGlobalObject* aGlobal, ElementType& aElement, bool* aWriteOnly,
     ErrorResult& aRv) {
@@ -452,18 +425,17 @@ static already_AddRefed<SourceSurface> G
           &aElement, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
 
   RefPtr<SourceSurface> surface = res.GetSourceSurface();
   if (NS_WARN_IF(!surface)) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
-  // check write-only mode
-  *aWriteOnly = !CheckSecurityForElements(res);
+  *aWriteOnly = res.mIsWriteOnly;
 
   return surface.forget();
 }
 
 ImageBitmap::ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData,
                          bool aWriteOnly, gfxAlphaType aAlphaType)
     : mParent(aGlobal),
       mData(aData),
@@ -866,17 +838,17 @@ already_AddRefed<ImageBitmap> ImageBitma
   if (aVideoEl.ReadyState() <= HAVE_METADATA) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   // Check security.
   nsCOMPtr<nsIPrincipal> principal = aVideoEl.GetCurrentVideoPrincipal();
   bool CORSUsed = aVideoEl.GetCORSMode() != CORS_NONE;
-  bool writeOnly = !CheckSecurityForElements(false, CORSUsed, principal);
+  bool writeOnly = CheckWriteOnlySecurity(CORSUsed, principal);
 
   // Create ImageBitmap.
   RefPtr<layers::Image> data = aVideoEl.GetCurrentImage();
   if (!data) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
   RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, writeOnly);
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsLayoutUtils.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/BasicEvents.h"
+#include "mozilla/dom/CanvasUtils.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/EffectSet.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/PathHelpers.h"
@@ -7508,19 +7509,20 @@ nsLayoutUtils::SurfaceFromElementResult 
   }
 
   int32_t corsmode;
   if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) {
     result.mCORSUsed = (corsmode != imgIRequest::CORS_NONE);
   }
 
   result.mPrincipal = principal.forget();
-  // no images, including SVG images, can load content from another domain.
-  result.mIsWriteOnly = false;
   result.mImageRequest = imgRequest.forget();
+  result.mIsWriteOnly =
+      CanvasUtils::CheckWriteOnlySecurity(result.mCORSUsed, result.mPrincipal);
+
   return result;
 }
 
 nsLayoutUtils::SurfaceFromElementResult nsLayoutUtils::SurfaceFromElement(
     HTMLImageElement* aElement, uint32_t aSurfaceFlags,
     RefPtr<DrawTarget>& aTarget) {
   return SurfaceFromElement(static_cast<nsIImageLoadingContent*>(aElement),
                             aSurfaceFlags, aTarget);
@@ -7603,17 +7605,18 @@ nsLayoutUtils::SurfaceFromElementResult 
       result.mSourceSurface = opt;
     }
   }
 
   result.mCORSUsed = aElement->GetCORSMode() != CORS_NONE;
   result.mHasSize = true;
   result.mSize = result.mLayersImage->GetSize();
   result.mPrincipal = principal.forget();
-  result.mIsWriteOnly = false;
+  result.mIsWriteOnly =
+      CanvasUtils::CheckWriteOnlySecurity(result.mCORSUsed, result.mPrincipal);
 
   return result;
 }
 
 nsLayoutUtils::SurfaceFromElementResult nsLayoutUtils::SurfaceFromElement(
     dom::Element* aElement, uint32_t aSurfaceFlags,
     RefPtr<DrawTarget>& aTarget) {
   // If it's a <canvas>, we may be able to just grab its internal surface
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.fillStyle.cross.html]
-  [Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.fillStyle.redirect.html]
-  [Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.strokeStyle.cross.html]
-  [Setting strokeStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.strokeStyle.redirect.html]
-  [Setting strokeStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.fillStyle.sub.html.ini
@@ -0,0 +1,3 @@
+[security.pattern.fillStyle.sub.html]
+  [redirected to same-origin HTMLVideoElement: Setting fillStyle to an origin-unclear pattern makes the canvas origin-unclean]
+    expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.fillStyle.cross.html]
-  [Setting fillStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.fillStyle.redirect.html]
-  [Setting fillStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.strokeStyle.cross.html]
-  [Setting strokeStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.strokeStyle.redirect.html]
-  [Setting strokeStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
--- a/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-origin.sub.html
+++ b/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-origin.sub.html
@@ -1,18 +1,18 @@
 <!DOCTYPE html>
 <meta charset=utf-8>
 <title>createImageBitmap: origin-clean flag</title>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="/common/media.js"></script>
 <script src="/common/namespaces.js"></script>
+<script src="/common/canvas-tests.js"></script>
 <div id=log></div>
 <script>
-const crossOriginImageUrl = "http://{{domains[www1]}}:{{ports[http][0]}}/images/red.png";
 
 function assert_origin_unclean_getImageData(bitmap) {
   const context = document.createElement("canvas").getContext("2d");
   context.drawImage(bitmap, 0, 0);
   assert_throws("SecurityError", () => {
     context.getImageData(0, 0, 1, 1);
   });
 }
@@ -26,105 +26,22 @@ function assert_origin_unclean_drawImage
 
 function assert_origin_unclean_transferFromImageBitmap(bitmap) {
   var canvas = document.createElement('canvas');
   var ctx = canvas.getContext('bitmaprenderer');
   ctx.transferFromImageBitmap(bitmap);
   assert_throws('SecurityError', () => canvas.toDataURL());
 }
 
-function makeImage() {
-  return new Promise((resolve, reject) => {
-    const image = new Image();
-    image.onload = () => resolve(image);
-    image.onerror = reject;
-    image.src = crossOriginImageUrl;
-  });
-}
-
-const arguments = [
-  {
-    name: "cross-origin HTMLImageElement",
-    factory: makeImage,
-  },
-
-  {
-    name: "cross-origin SVGImageElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const image = document.createElementNS(NAMESPACES.svg, "image");
-        image.onload = () => resolve(image);
-        image.onerror = reject;
-        image.setAttribute("externalResourcesRequired", "true");
-        image.setAttributeNS(NAMESPACES.xlink, 'xlink:href', crossOriginImageUrl);
-        document.body.appendChild(image);
-      });
-    },
-  },
-
-  {
-    name: "cross-origin HTMLVideoElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const video = document.createElement("video");
-        video.oncanplaythrough = () => resolve(video);
-        video.onerror = reject;
-        video.src = getVideoURI("http://{{domains[www1]}}:{{ports[http][0]}}/media/movie_300");
-      });
-    },
-  },
-
-  {
-    name: "redirected to cross-origin HTMLVideoElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const video = document.createElement("video");
-        video.oncanplaythrough = () => resolve(video);
-        video.onerror = reject;
-        video.src = "/common/redirect.py?location=" + getVideoURI("http://{{domains[www1]}}:{{ports[http][0]}}/media/movie_300");
-      });
-    },
-  },
-
-  {
-    name: "redirected to same-origin HTMLVideoElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const video = document.createElement("video");
-        video.oncanplaythrough = () => resolve(video);
-        video.onerror = reject;
-        video.src = "http://{{domains[www1]}}:{{ports[http][0]}}/common/redirect.py?location=" + getVideoURI("http://{{domains[]}}:{{ports[http][0]}}/media/movie_300");
-      });
-    },
-  },
-
-  {
-    name: "unclean HTMLCanvasElement",
-    factory: () => {
-      return makeImage().then(image => {
-        const canvas = document.createElement("canvas");
-        const context = canvas.getContext("2d");
-        context.drawImage(image, 0, 0);
-        return canvas;
-      });
-    },
-  },
-
-  {
-    name: "unclean ImageBitmap",
-    factory: () => {
-      return makeImage().then(createImageBitmap);
-    },
-  },
-];
-
-for (let { name, factory } of arguments) {
+forEachCanvasSource("http://{{domains[www1]}}:{{ports[http][0]}}",
+                    "http://{{domains[]}}:{{ports[http][0]}}",
+                    (name, factory) => {
   promise_test(function() {
     return factory().then(createImageBitmap).then(assert_origin_unclean_getImageData);
   }, `${name}: origin unclear getImageData`);
   promise_test(function() {
     return factory().then(createImageBitmap).then(assert_origin_unclean_drawImage);
   }, `${name}: origin unclear 2dContext.drawImage`);
   promise_test(function() {
     return factory().then(createImageBitmap).then(assert_origin_unclean_transferFromImageBitmap);
   }, `${name}: origin unclear bitmaprenderer.transferFromImageBitmap`);
-}
+});
 </script>
--- a/testing/web-platform/tests/common/canvas-tests.js
+++ b/testing/web-platform/tests/common/canvas-tests.js
@@ -102,8 +102,99 @@ function addCrossOriginRedirectYellowIma
 {
     var img = new Image();
     img.id = "yellow.png";
     img.className = "resource";
     img.src = get_host_info().HTTP_ORIGIN + "/common/redirect.py?location=" +
         get_host_info().HTTP_REMOTE_ORIGIN + "/images/yellow.png";
     document.body.appendChild(img);
 }
+
+function forEachCanvasSource(crossOriginUrl, sameOriginUrl, callback) {
+  function makeImage() {
+    return new Promise((resolve, reject) => {
+      const image = new Image();
+      image.onload = () => resolve(image);
+      image.onerror = reject;
+      image.src = crossOriginUrl + "/images/red.png";
+    });
+  }
+
+  const arguments = [
+    {
+      name: "cross-origin HTMLImageElement",
+      factory: makeImage,
+    },
+
+    {
+      name: "cross-origin SVGImageElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const image = document.createElementNS(NAMESPACES.svg, "image");
+          image.onload = () => resolve(image);
+          image.onerror = reject;
+          image.setAttribute("externalResourcesRequired", "true");
+          image.setAttributeNS(NAMESPACES.xlink, 'xlink:href', crossOriginUrl + "/images/red.png");
+          document.body.appendChild(image);
+        });
+      },
+    },
+
+    {
+      name: "cross-origin HTMLVideoElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const video = document.createElement("video");
+          video.oncanplaythrough = () => resolve(video);
+          video.onerror = reject;
+          video.src = getVideoURI(crossOriginUrl + "/media/movie_300");
+        });
+      },
+    },
+
+    {
+      name: "redirected to cross-origin HTMLVideoElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const video = document.createElement("video");
+          video.oncanplaythrough = () => resolve(video);
+          video.onerror = reject;
+          video.src = "/common/redirect.py?location=" + getVideoURI(crossOriginUrl + "/media/movie_300");
+        });
+      },
+    },
+
+    {
+      name: "redirected to same-origin HTMLVideoElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const video = document.createElement("video");
+          video.oncanplaythrough = () => resolve(video);
+          video.onerror = reject;
+          video.src = crossOriginUrl + "/common/redirect.py?location=" + getVideoURI(sameOriginUrl + "/media/movie_300");
+        });
+      },
+    },
+
+    {
+      name: "unclean HTMLCanvasElement",
+      factory: () => {
+        return makeImage().then(image => {
+          const canvas = document.createElement("canvas");
+          const context = canvas.getContext("2d");
+          context.drawImage(image, 0, 0);
+          return canvas;
+        });
+      },
+    },
+
+    {
+      name: "unclean ImageBitmap",
+      factory: () => {
+        return makeImage().then(createImageBitmap);
+      },
+    },
+  ];
+
+  for (let { name, factory } of arguments) {
+    callback(name, factory);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.fillStyle.sub.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by tools/gentest.py. -->
+<title>Canvas test: security.pattern.canvas.fillStyle.cross</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/media.js"></script>
+<script src="/common/namespaces.js"></script>
+<script src="/common/canvas-tests.js"></script>
+
+<body>
+<p class="desc">Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean</p>
+
+<script>
+
+forEachCanvasSource("http://{{domains[www1]}}:{{ports[http][0]}}",
+                    "http://{{domains[]}}:{{ports[http][0]}}",
+                    (name, factory) => {
+  promise_test(_ => {
+    return factory().then(source => {
+      const canvas = document.createElement('canvas');
+      const ctx = canvas.getContext('2d');
+      const pattern = ctx.createPattern(source, 'repeat');
+      ctx.fillStyle = pattern;
+      ctx.fillStyle = 'red';
+      assert_throws("SECURITY_ERR", function() { canvas.toDataURL(); });
+      assert_throws("SECURITY_ERR", function() { ctx.getImageData(0, 0, 1, 1); });
+    });
+  }, `${name}: Setting fillStyle to an origin-unclear pattern makes the canvas origin-unclean`);
+});
+
+</script>