2011-06-08 Kenneth Russell <kbr@google.com>
authorkbr@google.com <kbr@google.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 8 Jun 2011 21:27:51 +0000 (21:27 +0000)
committerkbr@google.com <kbr@google.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 8 Jun 2011 21:27:51 +0000 (21:27 +0000)
        Reviewed by Adam Barth.

        Disallow use of cross-domain media (images, video) in WebGL
        https://bugs.webkit.org/show_bug.cgi?id=62257

        Updated origin-clean-conformance.html to track upstream version in
        Khronos repository. Added new layout tests mirroring those added
        in bug 61015 which verify that new CORS support for images is
        working in the context of WebGL.

        Verified new tests in WebKit and Chromium.

        * http/tests/canvas/webgl/origin-clean-conformance-expected.txt:
        * http/tests/canvas/webgl/origin-clean-conformance.html:
        * http/tests/security/webgl-remote-read-remote-image-allowed-expected.txt: Added.
        * http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials-expected.txt: Added.
        * http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html: Added.
        * http/tests/security/webgl-remote-read-remote-image-allowed.html: Added.
        * http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin-expected.txt: Added.
        * http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html: Added.
        * platform/mac-wk2/Skipped:
2011-06-08  Kenneth Russell  <kbr@google.com>

        Reviewed by Adam Barth.

        Disallow use of cross-domain media (images, video) in WebGL
        https://bugs.webkit.org/show_bug.cgi?id=62257

        Updated WebGL implementation to track recent spec updates in this area.

        Tests: http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html
               http/tests/security/webgl-remote-read-remote-image-allowed.html
               http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html

        * html/canvas/CanvasRenderingContext.cpp:
        (WebCore::CanvasRenderingContext::wouldTaintOrigin):
        (WebCore::CanvasRenderingContext::checkOrigin):
        * html/canvas/CanvasRenderingContext.h:
        (WebCore::CanvasRenderingContext::checkOrigin):
        * html/canvas/WebGLRenderingContext.cpp:
        (WebCore::WebGLRenderingContext::readPixels):
        (WebCore::WebGLRenderingContext::texImage2D):
        (WebCore::WebGLRenderingContext::videoFrameToImage):
        (WebCore::WebGLRenderingContext::texSubImage2D):
        * html/canvas/WebGLRenderingContext.h:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@88387 268f45cc-cd09-0410-ab3c-d52691b4dbfc

15 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/canvas/webgl/origin-clean-conformance-expected.txt
LayoutTests/http/tests/canvas/webgl/origin-clean-conformance.html
LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html [new file with mode: 0644]
LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed.html [new file with mode: 0644]
LayoutTests/http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html [new file with mode: 0644]
LayoutTests/platform/mac-wk2/Skipped
Source/WebCore/ChangeLog
Source/WebCore/html/canvas/CanvasRenderingContext.cpp
Source/WebCore/html/canvas/CanvasRenderingContext.h
Source/WebCore/html/canvas/WebGLRenderingContext.cpp
Source/WebCore/html/canvas/WebGLRenderingContext.h

index d1728ff..49289b8 100644 (file)
@@ -1,3 +1,27 @@
+2011-06-08  Kenneth Russell  <kbr@google.com>
+
+        Reviewed by Adam Barth.
+
+        Disallow use of cross-domain media (images, video) in WebGL
+        https://bugs.webkit.org/show_bug.cgi?id=62257
+
+        Updated origin-clean-conformance.html to track upstream version in
+        Khronos repository. Added new layout tests mirroring those added
+        in bug 61015 which verify that new CORS support for images is
+        working in the context of WebGL.
+
+        Verified new tests in WebKit and Chromium.
+
+        * http/tests/canvas/webgl/origin-clean-conformance-expected.txt:
+        * http/tests/canvas/webgl/origin-clean-conformance.html:
+        * http/tests/security/webgl-remote-read-remote-image-allowed-expected.txt: Added.
+        * http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials-expected.txt: Added.
+        * http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html: Added.
+        * http/tests/security/webgl-remote-read-remote-image-allowed.html: Added.
+        * http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin-expected.txt: Added.
+        * http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html: Added.
+        * platform/mac-wk2/Skipped:
+
 2011-06-08  John Bauman  <jbauman@chromium.org>
 
         Reviewed by James Robinson.
index d205f1b..ca9217c 100644 (file)
@@ -1,31 +1,29 @@
-This test ensures WebGL implementations follow proper origin restrictions.
+This test ensures WebGL implementations follow proper same-origin restrictions.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
 PASS Playable video format found
 PASS img was loaded
 
-check that copying an img from another origin clears the origin-clean flag.
-PASS should not throw exception by readPixels for origin clean canvas.
-PASS should not throw exception by toDataURL for origin clean canvas.
-PASS should throw exception by readPixels for NON origin clean canvas.
-PASS should throw exception by toDataURL for NON origin clean canvas.
-
-check that copying from 1 unclean 3d canvas to another clears the origin-clean flag on the second canvas.
-PASS should not throw exception by readPixels for origin clean canvas.
-PASS should not throw exception by toDataURL for origin clean canvas.
-PASS should throw exception by readPixels for NON origin clean canvas.
-PASS should throw exception by toDataURL for NON origin clean canvas.
-
-check that copying from 1 unclean 3d canvas to a 2d canvas clears the origin-clean flag on the 2d canvas.
-PASS should not throw exception by toDataURL for origin clean canvas.
-PASS should throw exception by toDataURL for NON origin clean canvas.
-
-check that copying a video from another origin clears the origin-clean flag.
-PASS should not throw exception by readPixels for origin clean canvas.
-PASS should not throw exception by toDataURL for origin clean canvas.
-PASS should throw exception by readPixels for NON origin clean canvas.
+check that an attempt to upload an image from another origin throws an exception.
+PASS texImage2D with cross-origin image should throw exception.
+PASS texSubImage2D with cross-origin image should throw exception.
+check that readPixels and toDataURL continue to work against this canvas.
+PASS readPixels should never throw exception -- not possible to dirty origin of WebGL canvas.
+PASS should not throw exception by toDataURL for WebGL canvas, which should stay origin clean.
+check that an attempt to upload a tainted canvas throws an exception.
 PASS should throw exception by toDataURL for NON origin clean canvas.
+PASS texImage2D with NON origin clean canvas should throw exception.
+PASS texSubImage2D with NON origin clean canvas should throw exception.
+check that readPixels and toDataURL continue to work against this canvas.
+PASS readPixels should never throw exception -- not possible to dirty origin of WebGL canvas.
+PASS should not throw exception by toDataURL for WebGL canvas, which should stay origin clean.
+check that an attempt to upload a video from another origin throws an exception.
+PASS texImage2D with cross-origin video should throw exception.
+PASS texSubImage2D with cross-origin video should throw exception.
+check that readPixels and toDataURL continue to work against this canvas.
+PASS readPixels should never throw exception -- not possible to dirty origin of WebGL canvas.
+PASS should not throw exception by toDataURL for WebGL canvas, which should stay origin clean.
 
 TEST COMPLETE
-   
index 33bb054..e983616 100644 (file)
@@ -82,6 +82,8 @@ function causedException(func) {
   return hadException;
 }
 
+var testVideo = false;
+
 function init() {
   var video = document.getElementById("video");
 
@@ -108,6 +110,7 @@ function init() {
     video.src = videoFile;
     video.addEventListener("playing", runTests);
     video.play();
+    testVideo = true;
   } else {
     // Still run the other tests, even if the video failed.
     runTests();
@@ -115,10 +118,22 @@ function init() {
 }
 
 function runTests() {
-  description("This test ensures WebGL implementations follow proper origin restrictions.");
+  description("This test ensures WebGL implementations follow proper same-origin restrictions.");
   var img = document.getElementById("img");
   assertMsg(img.width > 0 && img.height > 0, "img was loaded");
 
+  function makeTexImage2D(gl, src) {
+    return function() {
+      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
+    };
+  }
+
+  function makeTexSubImage2D(gl, src) {
+    return function() {
+      gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, src);
+    };
+  }
+
   function makeReadPixels(gl) {
     return function() {
       var buf = new Uint8Array(4);
@@ -132,70 +147,56 @@ function runTests() {
     }
   }
 
-  debug("");
-  debug("check that copying an img from another origin clears the origin-clean flag.");
   var canvas1 = document.getElementById("canvas1");
-  var gl1 = create3DContext(canvas1);
-  assertMsg(!causedException(makeReadPixels(gl1)),
-            "should not throw exception by readPixels for origin clean canvas.");
-  assertMsg(!causedException(makeToDataURL(canvas1)),
-            "should not throw exception by toDataURL for origin clean canvas.");
-
-  var tex = gl1.createTexture();
-  gl1.bindTexture(gl1.TEXTURE_2D, tex);
-  gl1.texImage2D(gl1.TEXTURE_2D, 0, gl1.RGBA, gl1.RGBA, gl1.UNSIGNED_BYTE, img);
-
-  assertMsg(causedException(makeReadPixels(gl1)),
-            "should throw exception by readPixels for NON origin clean canvas.");
-  assertMsg(causedException(makeToDataURL(canvas1)),
-            "should throw exception by toDataURL for NON origin clean canvas.");
+  var gl = create3DContext(canvas1);
 
   debug("");
-  debug("check that copying from 1 unclean 3d canvas to another clears the origin-clean flag on the second canvas.");
-  var canvas2 = document.getElementById("canvas2");
-  var gl2 = create3DContext(canvas2);
-
-  assertMsg(!causedException(makeReadPixels(gl2)),
-            "should not throw exception by readPixels for origin clean canvas.");
-  assertMsg(!causedException(makeToDataURL(canvas2)),
-            "should not throw exception by toDataURL for origin clean canvas.");
-
-  var tex = gl2.createTexture();
-  gl2.bindTexture(gl2.TEXTURE_2D, tex);
-  gl2.texImage2D(
-     gl2.TEXTURE_2D, 0, gl2.RGBA, gl2.RGBA, gl2.UNSIGNED_BYTE, canvas1);
+  debug("check that an attempt to upload an image from another origin throws an exception.");
+  var tex = gl.createTexture();
+  gl.bindTexture(gl.TEXTURE_2D, tex);
+  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+  assertMsg(causedException(makeTexImage2D(gl, img)),
+            "texImage2D with cross-origin image should throw exception.");
+  assertMsg(causedException(makeTexSubImage2D(gl, img)),
+            "texSubImage2D with cross-origin image should throw exception.");
+
+  debug("check that readPixels and toDataURL continue to work against this canvas.");
+  assertMsg(!causedException(makeReadPixels(gl)),
+            "readPixels should never throw exception -- not possible to dirty origin of WebGL canvas.");
+  assertMsg(!causedException(makeToDataURL(canvas1)),
+            "should not throw exception by toDataURL for WebGL canvas, which should stay origin clean.");
 
-  assertMsg(causedException(makeReadPixels(gl2)),
-            "should throw exception by readPixels for NON origin clean canvas.");
+  debug("check that an attempt to upload a tainted canvas throws an exception.");
+  var canvas2 = document.getElementById("canvas2");
+  var ctx2d = canvas2.getContext("2d");
+  ctx2d.drawImage(img, 0, 0);
   assertMsg(causedException(makeToDataURL(canvas2)),
             "should throw exception by toDataURL for NON origin clean canvas.");
-
-  debug("");
-  debug("check that copying from 1 unclean 3d canvas to a 2d canvas clears the origin-clean flag on the 2d canvas.");
-  var canvas3 = document.getElementById("canvas3");
-  var ctx2d = canvas3.getContext("2d");
-  assertMsg(!causedException(makeToDataURL(canvas3)),
-            "should not throw exception by toDataURL for origin clean canvas.");
-  ctx2d.drawImage(canvas2, 0, 0);
-  assertMsg(causedException(makeToDataURL(canvas3)),
-            "should throw exception by toDataURL for NON origin clean canvas.");
-
-  debug("");
-  debug("check that copying a video from another origin clears the origin-clean flag.");
-  var canvas4 = document.getElementById("canvas4");
-  var gl4 = create3DContext(canvas4);
-  assertMsg(!causedException(makeReadPixels(gl4)),
-            "should not throw exception by readPixels for origin clean canvas.");
-  assertMsg(!causedException(makeToDataURL(canvas4)),
-            "should not throw exception by toDataURL for origin clean canvas.");
-  var tex4 = gl4.createTexture();
-  gl4.bindTexture(gl4.TEXTURE_2D, tex4);
-  gl4.texImage2D(
-      gl4.TEXTURE_2D, 0, gl4.RGBA, gl4.RGBA, gl4.UNSIGNED_BYTE, video);
-  assertMsg(causedException(makeReadPixels(gl4)),
-            "should throw exception by readPixels for NON origin clean canvas.");
-  assertMsg(causedException(makeToDataURL(canvas4)),
-            "should throw exception by toDataURL for NON origin clean canvas.");
+  assertMsg(causedException(makeTexImage2D(gl, canvas2)),
+            "texImage2D with NON origin clean canvas should throw exception.");
+  assertMsg(causedException(makeTexSubImage2D(gl, canvas2)),
+            "texSubImage2D with NON origin clean canvas should throw exception.");
+
+  debug("check that readPixels and toDataURL continue to work against this canvas.");
+  assertMsg(!causedException(makeReadPixels(gl)),
+            "readPixels should never throw exception -- not possible to dirty origin of WebGL canvas.");
+  assertMsg(!causedException(makeToDataURL(canvas1)),
+            "should not throw exception by toDataURL for WebGL canvas, which should stay origin clean.");
+
+  if (testVideo) {
+    debug("check that an attempt to upload a video from another origin throws an exception.");
+    var video = document.getElementById("video");
+    assertMsg(causedException(makeTexImage2D(gl, video)),
+              "texImage2D with cross-origin video should throw exception.");
+    assertMsg(causedException(makeTexSubImage2D(gl, video)),
+              "texSubImage2D with cross-origin video should throw exception.");
+
+    debug("check that readPixels and toDataURL continue to work against this canvas.");
+    assertMsg(!causedException(makeReadPixels(gl)),
+              "readPixels should never throw exception -- not possible to dirty origin of WebGL canvas.");
+    assertMsg(!causedException(makeToDataURL(canvas1)),
+              "should not throw exception by toDataURL for WebGL canvas, which should stay origin clean.");
+  }
 
   debug('<br /><span class="pass">TEST COMPLETE</span>');
   if (window.layoutTestController)
@@ -211,8 +212,6 @@ function runTests() {
 <div id="console"></div>
 <canvas id="canvas1"></canvas>
 <canvas id="canvas2"></canvas>
-<canvas id="canvas3"></canvas>
-<canvas id="canvas4"></canvas>
 <img id="img" src="http://localhost:8000/local/resources/abe.png" style="display:none;">
 <video id="video" style="display:none;"/>
 </body>
diff --git a/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-expected.txt b/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-expected.txt
new file mode 100644 (file)
index 0000000..c86861c
--- /dev/null
@@ -0,0 +1,13 @@
+Untainted canvas:
+PASS: Calling readPixels() from an untainted canvas was allowed.
+PASS: Calling toDataURL() on an untainted canvas was allowed.
+
+
+Tainted canvas:
+PASS: Calling texImage2D() with an untainted image was allowed
+PASS: Calling readPixels() from a canvas tainted by a remote image was allowed.
+PASS: Calling toDataURL() on a canvas CORS-untainted by a remote image was allowed.
+PASS: Calling texImage2D() with an untainted canvas was allowed
+PASS: Calling readPixels() from a canvas tainted by a CORS-untained canvas was allowed.
+PASS: Calling toDataURL() on a canvas CORS-untainted by a CORS-untained canvas was allowed.
+
diff --git a/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials-expected.txt b/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials-expected.txt
new file mode 100644 (file)
index 0000000..c86861c
--- /dev/null
@@ -0,0 +1,13 @@
+Untainted canvas:
+PASS: Calling readPixels() from an untainted canvas was allowed.
+PASS: Calling toDataURL() on an untainted canvas was allowed.
+
+
+Tainted canvas:
+PASS: Calling texImage2D() with an untainted image was allowed
+PASS: Calling readPixels() from a canvas tainted by a remote image was allowed.
+PASS: Calling toDataURL() on a canvas CORS-untainted by a remote image was allowed.
+PASS: Calling texImage2D() with an untainted canvas was allowed
+PASS: Calling readPixels() from a canvas tainted by a CORS-untained canvas was allowed.
+PASS: Calling toDataURL() on a canvas CORS-untainted by a CORS-untained canvas was allowed.
+
diff --git a/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html b/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html
new file mode 100644 (file)
index 0000000..c09e37b
--- /dev/null
@@ -0,0 +1,101 @@
+<pre id="console"></pre>
+<script>
+if (window.layoutTestController) {
+    layoutTestController.overridePreference("WebKitWebGLEnabled", "1");
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+log = function(msg)
+{
+    document.getElementById('console').appendChild(document.createTextNode(msg + "\n"));
+}
+
+testTexImage2D = function(gl, source, description)
+{
+    description = "Calling texImage2D() with an untainted " + description;
+    try {
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
+        log("PASS: " + description + " was allowed");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed: Threw error: " + e + ".");
+    }
+}
+
+testReadPixels = function(gl, description)
+{
+    description = "Calling readPixels() from a canvas tainted by a " + description;
+    try {
+        var pixels = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+        log("PASS: " + description + " was allowed.");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed - Threw error: " + e + ".");
+    }
+}
+
+testToDataURL = function(canvas, description)
+{
+    description = "Calling toDataURL() on a canvas CORS-untainted by a " + description;
+    try {
+        var dataURL = canvas.toDataURL();
+        log("PASS: " + description + " was allowed.");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed - Threw error: " + e + ".");
+    }
+}
+
+test = function(canvas, description)
+{
+    testReadPixels(canvas.getContext("experimental-webgl"), description);
+    testToDataURL(canvas, description);
+}
+
+var image = new Image();
+image.onload = function() {
+    var canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var gl = canvas.getContext("experimental-webgl");
+
+    // Control tests
+    log("Untainted canvas:");
+    try {
+        var pixels = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+        log("PASS: Calling readPixels() from an untainted canvas was allowed.");
+    } catch (e) {
+        log("FAIL: Calling readPixels() from an untainted canvas was not allowed: Threw error: " + e + ".");
+    }
+    try {
+        var dataURL = canvas.toDataURL();
+        log("PASS: Calling toDataURL() on an untainted canvas was allowed.");
+    } catch (e) {
+        log("FAIL: Calling toDataURL() on an untainted canvas was not allowed: Threw error: " + e + ".");
+    }
+
+    log("\n");
+    log("Tainted canvas:");
+    // Test reading from a canvas after uploading a remote image as a texture
+    var texture = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, texture);
+    testTexImage2D(gl, image, "image");
+
+    test(canvas, "remote image");
+
+    // Now test reading from a canvas after uploading a tainted canvas onto it
+    var dirtyCanvas = document.createElement("canvas");
+    dirtyCanvas.width = 100;
+    dirtyCanvas.height = 100;
+    var dirtyContext = dirtyCanvas.getContext("2d");
+    dirtyContext.drawImage(image, 0, 0, 100, 100);
+    testTexImage2D(gl, dirtyCanvas, "canvas");
+
+    test(canvas, "CORS-untained canvas");
+
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+image.crossOrigin = "use-credentials";
+image.src = "http://localhost:8000/security/resources/abe-allow-credentials.php";
+</script>
diff --git a/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed.html b/LayoutTests/http/tests/security/webgl-remote-read-remote-image-allowed.html
new file mode 100644 (file)
index 0000000..6448c2b
--- /dev/null
@@ -0,0 +1,101 @@
+<pre id="console"></pre>
+<script>
+if (window.layoutTestController) {
+    layoutTestController.overridePreference("WebKitWebGLEnabled", "1");
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+log = function(msg)
+{
+    document.getElementById('console').appendChild(document.createTextNode(msg + "\n"));
+}
+
+testTexImage2D = function(gl, source, description)
+{
+    description = "Calling texImage2D() with an untainted " + description;
+    try {
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
+        log("PASS: " + description + " was allowed");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed: Threw error: " + e + ".");
+    }
+}
+
+testReadPixels = function(gl, description)
+{
+    description = "Calling readPixels() from a canvas tainted by a " + description;
+    try {
+        var pixels = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+        log("PASS: " + description + " was allowed.");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed - Threw error: " + e + ".");
+    }
+}
+
+testToDataURL = function(canvas, description)
+{
+    description = "Calling toDataURL() on a canvas CORS-untainted by a " + description;
+    try {
+        var dataURL = canvas.toDataURL();
+        log("PASS: " + description + " was allowed.");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed - Threw error: " + e + ".");
+    }
+}
+
+test = function(canvas, description)
+{
+    testReadPixels(canvas.getContext("experimental-webgl"), description);
+    testToDataURL(canvas, description);
+}
+
+var image = new Image();
+image.onload = function() {
+    var canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var gl = canvas.getContext("experimental-webgl");
+
+    // Control tests
+    log("Untainted canvas:");
+    try {
+        var pixels = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+        log("PASS: Calling readPixels() from an untainted canvas was allowed.");
+    } catch (e) {
+        log("FAIL: Calling readPixels() from an untainted canvas was not allowed: Threw error: " + e + ".");
+    }
+    try {
+        var dataURL = canvas.toDataURL();
+        log("PASS: Calling toDataURL() on an untainted canvas was allowed.");
+    } catch (e) {
+        log("FAIL: Calling toDataURL() on an untainted canvas was not allowed: Threw error: " + e + ".");
+    }
+
+    log("\n");
+    log("Tainted canvas:");
+    // Test reading from a canvas after uploading a remote image as a texture
+    var texture = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, texture);
+    testTexImage2D(gl, image, "image");
+
+    test(canvas, "remote image");
+
+    // Now test reading from a canvas after uploading a tainted canvas onto it
+    var dirtyCanvas = document.createElement("canvas");
+    dirtyCanvas.width = 100;
+    dirtyCanvas.height = 100;
+    var dirtyContext = dirtyCanvas.getContext("2d");
+    dirtyContext.drawImage(image, 0, 0, 100, 100);
+    testTexImage2D(gl, dirtyCanvas, "canvas");
+
+    test(canvas, "CORS-untained canvas");
+
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+image.crossOrigin = "anonymous";
+image.src = "http://localhost:8000/security/resources/abe-allow-star.php";
+</script>
diff --git a/LayoutTests/http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin-expected.txt b/LayoutTests/http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin-expected.txt
new file mode 100644 (file)
index 0000000..ee74457
--- /dev/null
@@ -0,0 +1,13 @@
+Untainted canvas:
+PASS: Calling readPixels() from an untainted canvas was allowed.
+PASS: Calling toDataURL() on an untainted canvas was allowed.
+
+
+Tainted canvas:
+PASS: Calling texImage2D() with a tainted image was not allowed: Threw error: Error: SECURITY_ERR: DOM Exception 18.
+PASS: Calling readPixels() on a canvas where tainting was attempted by a remote image was allowed.
+PASS: Calling toDataURL() on a canvas where tainting was attempted by a remote image was allowed.
+PASS: Calling texImage2D() with a tainted canvas was not allowed: Threw error: Error: SECURITY_ERR: DOM Exception 18.
+PASS: Calling readPixels() on a canvas where tainting was attempted by a tainted canvas was allowed.
+PASS: Calling toDataURL() on a canvas where tainting was attempted by a tainted canvas was allowed.
+
diff --git a/LayoutTests/http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html b/LayoutTests/http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html
new file mode 100644 (file)
index 0000000..f870c3e
--- /dev/null
@@ -0,0 +1,103 @@
+<pre id="console"></pre>
+<script>
+if (window.layoutTestController) {
+    layoutTestController.overridePreference("WebKitWebGLEnabled", "1");
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+log = function(msg)
+{
+    document.getElementById('console').appendChild(document.createTextNode(msg + "\n"));
+}
+
+testTexImage2D = function(gl, source, description)
+{
+    description = "Calling texImage2D() with a tainted " + description;
+    try {
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
+        log("FAIL: " + description + " was allowed");
+    } catch (e) {
+        log("PASS: " + description + " was not allowed: Threw error: " + e + ".");
+    }
+}
+
+testReadPixels = function(gl, description)
+{
+    description = "Calling readPixels() on a canvas where tainting was attempted by a " + description;
+    try {
+        var pixels = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+        log("PASS: " + description + " was allowed.");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed - Threw error: " + e + ".");
+    }
+}
+
+testToDataURL = function(canvas, description)
+{
+    description = "Calling toDataURL() on a canvas where tainting was attempted by a " + description;
+    try {
+        var dataURL = canvas.toDataURL();
+        log("PASS: " + description + " was allowed.");
+    } catch (e) {
+        log("FAIL: " + description + " was not allowed - Threw error: " + e + ".");
+    }
+}
+
+test = function(canvas, description)
+{
+    testReadPixels(canvas.getContext("experimental-webgl"), description);
+    testToDataURL(canvas, description);
+}
+
+var image = new Image();
+image.onload = function() {
+    var canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var gl = canvas.getContext("experimental-webgl");
+
+    // Control tests
+    log("Untainted canvas:");
+    try {
+        var pixels = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+        log("PASS: Calling readPixels() from an untainted canvas was allowed.");
+    } catch (e) {
+        log("FAIL: Calling readPixels() from an untainted canvas was not allowed: Threw error: " + e + ".");
+    }
+    try {
+        var dataURL = canvas.toDataURL();
+        log("PASS: Calling toDataURL() on an untainted canvas was allowed.");
+    } catch (e) {
+        log("FAIL: Calling toDataURL() on an untainted canvas was not allowed: Threw error: " + e + ".");
+    }
+
+    log("\n");
+    log("Tainted canvas:");
+    // Test reading from a canvas after uploading a remote image as a texture
+    var texture = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, texture);
+    testTexImage2D(gl, image, "image");
+
+    test(canvas, "remote image");
+
+    var dirtyCanvas = canvas;
+
+    // Now test reading from a canvas after drawing a tainted canvas onto it
+    var dirtyCanvas = document.createElement("canvas");
+    dirtyCanvas.width = 100;
+    dirtyCanvas.height = 100;
+    var dirtyContext = dirtyCanvas.getContext("2d");
+    dirtyContext.drawImage(image, 0, 0, 100, 100);
+    testTexImage2D(gl, dirtyCanvas, "canvas");
+
+    test(canvas, "tainted canvas");
+
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+// Notice that we forget to set the image.crossOrigin property!
+image.src = "http://localhost:8000/security/resources/abe-allow-star.php";
+</script>
index 7563b75..0148f12 100644 (file)
@@ -984,6 +984,9 @@ http/tests/inspector/network/ping.html
 http/tests/navigation/ping-cross-origin-from-https.html
 http/tests/navigation/ping-cross-origin.html
 http/tests/navigation/ping-same-origin.html
+http/tests/security/webgl-remote-read-remote-image-allowed.html
+http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html
+http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html
 loader/go-back-to-different-window-size.html
 media/restore-from-page-cache.html
 java/embedding-java-with-object.html
index b183222..0a59103 100644 (file)
@@ -1,3 +1,28 @@
+2011-06-08  Kenneth Russell  <kbr@google.com>
+
+        Reviewed by Adam Barth.
+
+        Disallow use of cross-domain media (images, video) in WebGL
+        https://bugs.webkit.org/show_bug.cgi?id=62257
+
+        Updated WebGL implementation to track recent spec updates in this area.
+
+        Tests: http/tests/security/webgl-remote-read-remote-image-allowed-with-credentials.html
+               http/tests/security/webgl-remote-read-remote-image-allowed.html
+               http/tests/security/webgl-remote-read-remote-image-blocked-no-crossorigin.html
+
+        * html/canvas/CanvasRenderingContext.cpp:
+        (WebCore::CanvasRenderingContext::wouldTaintOrigin):
+        (WebCore::CanvasRenderingContext::checkOrigin):
+        * html/canvas/CanvasRenderingContext.h:
+        (WebCore::CanvasRenderingContext::checkOrigin):
+        * html/canvas/WebGLRenderingContext.cpp:
+        (WebCore::WebGLRenderingContext::readPixels):
+        (WebCore::WebGLRenderingContext::texImage2D):
+        (WebCore::WebGLRenderingContext::videoFrameToImage):
+        (WebCore::WebGLRenderingContext::texSubImage2D):
+        * html/canvas/WebGLRenderingContext.h:
+
 2011-06-08  John Bauman  <jbauman@chromium.org>
 
         Reviewed by James Robinson.
index 7ba6c02..4e12ac4 100644 (file)
@@ -41,53 +41,73 @@ CanvasRenderingContext::CanvasRenderingContext(HTMLCanvasElement* canvas)
 {
 }
 
-void CanvasRenderingContext::checkOrigin(const CanvasPattern* pattern)
+bool CanvasRenderingContext::wouldTaintOrigin(const CanvasPattern* pattern)
 {
     if (canvas()->originClean() && pattern && !pattern->originClean())
-        canvas()->setOriginTainted();
+        return true;
+    return false;
 }
 
-void CanvasRenderingContext::checkOrigin(const HTMLCanvasElement* sourceCanvas)
+bool CanvasRenderingContext::wouldTaintOrigin(const HTMLCanvasElement* sourceCanvas)
 {
     if (canvas()->originClean() && sourceCanvas && !sourceCanvas->originClean())
-        canvas()->setOriginTainted();
+        return true;
+    return false;
 }
 
-void CanvasRenderingContext::checkOrigin(const HTMLImageElement* image)
+bool CanvasRenderingContext::wouldTaintOrigin(const HTMLImageElement* image)
 {
     if (!image || !canvas()->originClean())
-        return;
+        return false;
 
     CachedImage* cachedImage = image->cachedImage();
-    if (!cachedImage->passesAccessControlCheck(canvas()->securityOrigin()))
-        checkOrigin(cachedImage->response().url());
+    if (!cachedImage->passesAccessControlCheck(canvas()->securityOrigin())) {
+        if (wouldTaintOrigin(cachedImage->response().url()))
+            return true;
+    }
 
-    if (canvas()->originClean() && !cachedImage->image()->hasSingleSecurityOrigin())
-        canvas()->setOriginTainted();
+    if (!cachedImage->image()->hasSingleSecurityOrigin())
+        return true;
+
+    return false;
 }
 
-void CanvasRenderingContext::checkOrigin(const HTMLVideoElement* video)
+bool CanvasRenderingContext::wouldTaintOrigin(const HTMLVideoElement* video)
 {
 #if ENABLE(VIDEO)
     // FIXME: This check is likely wrong when a redirect is involved. We need
     // to test the finalURL. Please be careful when fixing this issue not to
     // make currentSrc be the final URL because then the
     // HTMLMediaElement.currentSrc DOM API would leak redirect destinations!
-    checkOrigin(video->currentSrc());
-    if (canvas()->originClean() && video && !video->hasSingleSecurityOrigin())
-        canvas()->setOriginTainted();
+    if (!video || !canvas()->originClean())
+        return false;
+
+    if (wouldTaintOrigin(video->currentSrc()))
+        return true;
+
+    if (!video->hasSingleSecurityOrigin())
+        return true;
 #endif
+
+    return false;
 }
 
-void CanvasRenderingContext::checkOrigin(const KURL& url)
+bool CanvasRenderingContext::wouldTaintOrigin(const KURL& url)
 {
-    if (!canvas()->originClean() || m_cleanOrigins.contains(url.string()))
-        return;
+    if (!canvas()->originClean() || m_cleanURLs.contains(url.string()))
+        return false;
 
     if (canvas()->securityOrigin()->taintsCanvas(url))
+        return true;
+
+    m_cleanURLs.add(url.string());
+    return false;
+}
+
+void CanvasRenderingContext::checkOrigin(const KURL& url)
+{
+    if (wouldTaintOrigin(url))
         canvas()->setOriginTainted();
-    else
-        m_cleanOrigins.add(url.string());
 }
 
 } // namespace WebCore
index a22773a..5327878 100644 (file)
@@ -63,15 +63,22 @@ public:
 
 protected:
     CanvasRenderingContext(HTMLCanvasElement*);
-    void checkOrigin(const CanvasPattern*);
-    void checkOrigin(const HTMLCanvasElement*);
-    void checkOrigin(const HTMLImageElement*);
-    void checkOrigin(const HTMLVideoElement*);
+    bool wouldTaintOrigin(const CanvasPattern*);
+    bool wouldTaintOrigin(const HTMLCanvasElement*);
+    bool wouldTaintOrigin(const HTMLImageElement*);
+    bool wouldTaintOrigin(const HTMLVideoElement*);
+    bool wouldTaintOrigin(const KURL&);
+
+    template<class T> void checkOrigin(const T* arg)
+    {
+        if (wouldTaintOrigin(arg))
+            canvas()->setOriginTainted();
+    }
     void checkOrigin(const KURL&);
 
 private:
     HTMLCanvasElement* m_canvas;
-    HashSet<String> m_cleanOrigins;
+    HashSet<String> m_cleanURLs;
 };
 
 } // namespace WebCore
index fd9d598..14ad6c5 100644 (file)
@@ -2807,14 +2807,13 @@ void WebGLRenderingContext::polygonOffset(GC3Dfloat factor, GC3Dfloat units)
     cleanupAfterGraphicsCall(false);
 }
 
-void WebGLRenderingContext::readPixels(GC3Dint x, GC3Dint y, GC3Dsizei width, GC3Dsizei height, GC3Denum format, GC3Denum type, ArrayBufferView* pixels, ExceptionCode& ec)
+void WebGLRenderingContext::readPixels(GC3Dint x, GC3Dint y, GC3Dsizei width, GC3Dsizei height, GC3Denum format, GC3Denum type, ArrayBufferView* pixels, ExceptionCode&)
 {
     if (isContextLost())
         return;
-    if (!canvas()->originClean()) {
-        ec = SECURITY_ERR;
-        return;
-    }
+    // Due to WebGL's same-origin restrictions, it is not possible to
+    // taint the origin using the WebGL API.
+    ASSERT(canvas()->originClean());
     // Validate input parameters.
     if (!pixels) {
         m_context->synthesizeGLError(GraphicsContext3D::INVALID_VALUE);
@@ -3157,7 +3156,11 @@ void WebGLRenderingContext::texImage2D(GC3Denum target, GC3Dint level, GC3Denum
         return;
     if (!validateHTMLImageElement(image))
         return;
-    checkOrigin(image);
+    if (wouldTaintOrigin(image)) {
+        ec = SECURITY_ERR;
+        return;
+    }
+
     texImage2DImpl(target, level, internalformat, format, type, image->cachedImage()->image(),
                    m_unpackFlipY, m_unpackPremultiplyAlpha, ec);
 }
@@ -3172,7 +3175,10 @@ void WebGLRenderingContext::texImage2D(GC3Denum target, GC3Dint level, GC3Denum
         m_context->synthesizeGLError(GraphicsContext3D::INVALID_VALUE);
         return;
     }
-    checkOrigin(canvas);
+    if (wouldTaintOrigin(canvas)) {
+        ec = SECURITY_ERR;
+        return;
+    }
     RefPtr<ImageData> imageData = canvas->getImageData();
     if (imageData)
         texImage2D(target, level, internalformat, format, type, imageData.get(), ec);
@@ -3182,7 +3188,7 @@ void WebGLRenderingContext::texImage2D(GC3Denum target, GC3Dint level, GC3Denum
 }
 
 #if ENABLE(VIDEO)
-PassRefPtr<Image> WebGLRenderingContext::videoFrameToImage(HTMLVideoElement* video)
+PassRefPtr<Image> WebGLRenderingContext::videoFrameToImage(HTMLVideoElement* video, ExceptionCode& ec)
 {
     if (!video || !video->videoWidth() || !video->videoHeight()) {
         m_context->synthesizeGLError(GraphicsContext3D::INVALID_VALUE);
@@ -3194,7 +3200,10 @@ PassRefPtr<Image> WebGLRenderingContext::videoFrameToImage(HTMLVideoElement* vid
         m_context->synthesizeGLError(GraphicsContext3D::OUT_OF_MEMORY);
         return 0;
     }
-    checkOrigin(video);
+    if (wouldTaintOrigin(video)) {
+        ec = SECURITY_ERR;
+        return 0;
+    }
     IntRect destRect(0, 0, size.width(), size.height());
     // FIXME: Turn this into a GPU-GPU texture copy instead of CPU readback.
     video->paintCurrentFrameInContext(buf->context(), destRect);
@@ -3207,8 +3216,8 @@ void WebGLRenderingContext::texImage2D(GC3Denum target, GC3Dint level, GC3Denum
     ec = 0;
     if (isContextLost())
         return;
-    RefPtr<Image> image = videoFrameToImage(video);
-    if (!video)
+    RefPtr<Image> image = videoFrameToImage(video, ec);
+    if (!image)
         return;
     texImage2DImpl(target, level, internalformat, format, type, image.get(), m_unpackFlipY, m_unpackPremultiplyAlpha, ec);
 }
@@ -3349,7 +3358,10 @@ void WebGLRenderingContext::texSubImage2D(GC3Denum target, GC3Dint level, GC3Din
         return;
     if (!validateHTMLImageElement(image))
         return;
-    checkOrigin(image);
+    if (wouldTaintOrigin(image)) {
+        ec = SECURITY_ERR;
+        return;
+    }
     texSubImage2DImpl(target, level, xoffset, yoffset, format, type, image->cachedImage()->image(),
                       m_unpackFlipY, m_unpackPremultiplyAlpha, ec);
 }
@@ -3364,7 +3376,10 @@ void WebGLRenderingContext::texSubImage2D(GC3Denum target, GC3Dint level, GC3Din
         m_context->synthesizeGLError(GraphicsContext3D::INVALID_VALUE);
         return;
     }
-    checkOrigin(canvas);
+    if (wouldTaintOrigin(canvas)) {
+        ec = SECURITY_ERR;
+        return;
+    }
     RefPtr<ImageData> imageData = canvas->getImageData();
     if (imageData)
         texSubImage2D(target, level, xoffset, yoffset, format, type, imageData.get(), ec);
@@ -3380,8 +3395,8 @@ void WebGLRenderingContext::texSubImage2D(GC3Denum target, GC3Dint level, GC3Din
     ec = 0;
     if (isContextLost())
         return;
-    RefPtr<Image> image = videoFrameToImage(video);
-    if (!video)
+    RefPtr<Image> image = videoFrameToImage(video, ec);
+    if (!image)
         return;
     texSubImage2DImpl(target, level, xoffset, yoffset, format, type, image.get(), m_unpackFlipY, m_unpackPremultiplyAlpha, ec);
 }
index 7792314..2e0f850 100644 (file)
@@ -344,7 +344,7 @@ public:
     bool validateWebGLObject(WebGLObject*);
 
 #if ENABLE(VIDEO)
-    PassRefPtr<Image> videoFrameToImage(HTMLVideoElement*);
+    PassRefPtr<Image> videoFrameToImage(HTMLVideoElement*, ExceptionCode&);
 #endif
 
     RefPtr<GraphicsContext3D> m_context;