Bug 1540221 - Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean, r=aosmond a=pascalc
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 16 Apr 2019 06:58:29 +0000
changeset 523305 3a1b53518d5ff472eb17f214bb02fc82b1675890
parent 523304 9c937da2511daebe7f34ad3a89e53c87ff56ae5e
child 523306 198b3248e141024d06c7603d4ad5c19f6f40cc11
push id11138
push userarchaeopteryx@coole-files.de
push dateTue, 23 Apr 2019 19:02:09 +0000
treeherdermozilla-beta@c53e3fd76964 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaosmond, pascalc
bugs1540221
milestone67.0
Bug 1540221 - Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean, r=aosmond a=pascalc 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
@@ -1888,17 +1888,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;
@@ -2056,23 +2060,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;
@@ -4184,18 +4189,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"
@@ -7390,19 +7391,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);
@@ -7485,17 +7487,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>