Implement the HTML5 canvas tainting rules to prevent potential data leakage
authoroliver@apple.com <oliver@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 7 Mar 2008 07:45:38 +0000 (07:45 +0000)
committeroliver@apple.com <oliver@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 7 Mar 2008 07:45:38 +0000 (07:45 +0000)
Reviewed by Mitz

Added originClean to HTMLCanvasElement and CanvasPattern
to track whether a canvas (or pattern) is tainted by remote
data.
Use originClean flag to determine whether getImageData should
return, well, image data.

Test: http/tests/security/canvas-remote-read-remote-image.html

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

LayoutTests/ChangeLog
LayoutTests/http/tests/security/canvas-remote-read-remote-image-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/canvas-remote-read-remote-image.html [new file with mode: 0644]
WebCore/ChangeLog
WebCore/html/CanvasPattern.cpp
WebCore/html/CanvasPattern.h
WebCore/html/CanvasRenderingContext2D.cpp
WebCore/html/CanvasRenderingContext2D.h
WebCore/html/HTMLCanvasElement.cpp
WebCore/html/HTMLCanvasElement.h

index da2d2ed..0f44c9d 100644 (file)
@@ -1,3 +1,12 @@
+2008-03-06  Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Mitz.
+
+        Test security restrictions for retrieving the content of tainted canvases
+
+        * http/tests/security/canvas-remote-read-remote-image-expected.txt: Added.
+        * http/tests/security/canvas-remote-read-remote-image.html: Added.
+
 2008-03-06  Adele Peterson  <adele@apple.com>
 
         Reviewed by Darin.
diff --git a/LayoutTests/http/tests/security/canvas-remote-read-remote-image-expected.txt b/LayoutTests/http/tests/security/canvas-remote-read-remote-image-expected.txt
new file mode 100644 (file)
index 0000000..2c66908
--- /dev/null
@@ -0,0 +1,14 @@
+CONSOLE MESSAGE: line 1: Call to getImageData failed due to tainted canvas.
+
+CONSOLE MESSAGE: line 1: Call to getImageData failed due to tainted canvas.
+
+CONSOLE MESSAGE: line 1: Call to getImageData failed due to tainted canvas.
+
+CONSOLE MESSAGE: line 1: Call to getImageData failed due to tainted canvas.
+
+PASS: Reading data from an untainted canvas was allowed
+PASS: Reading data from a canvas tainted by a remote image was not allowed
+PASS: Reading data from a canvas tainted by a tainted canvas was not allowed
+PASS: Reading data from a canvas tainted by a remote image tainted pattern was not allowed
+PASS: Reading data from a canvas tainted by a tainted canvas pattern was not allowed
+
diff --git a/LayoutTests/http/tests/security/canvas-remote-read-remote-image.html b/LayoutTests/http/tests/security/canvas-remote-read-remote-image.html
new file mode 100644 (file)
index 0000000..8c80bcd
--- /dev/null
@@ -0,0 +1,77 @@
+<div id="log"></div>
+<div id="console"></div>
+<script>
+if (window.layoutTestController) {
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+var image = new Image();
+image.onload = function() {
+    var canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var context = canvas.getContext("2d");
+    if (context.getImageData(0,0,100,100)) {
+        document.getElementById("console").innerHTML += "PASS: Reading data from an untainted canvas was allowed<br/>";
+    } else {
+        document.getElementById("console").innerHTML = "FAIL: Reading data from an untainted canvas was not allowed<br/>";
+    }
+    context.drawImage(image, 0, 0, 100, 100);
+    if (context.getImageData(0,0,100,100) == null) {
+        document.getElementById("console").innerHTML += "PASS: Reading data from a canvas tainted by a remote image was not allowed<br/>";
+    } else {
+        document.getElementById("console").innerHTML += "FAIL: Reading data from a canvas tainted by a remote image was allowed<br/>";
+    }
+    document.getElementById("log").appendChild(canvas);
+    var dirtyCanvas = canvas;
+    
+    // Now test reading from a canvas after drawing a tainted canvas onto it
+    canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var context = canvas.getContext("2d");
+    context.drawImage(dirtyCanvas, 0, 0, 100, 100);
+    if (context.getImageData(0,0,100,100) == null) {
+        document.getElementById("console").innerHTML += "PASS: Reading data from a canvas tainted by a tainted canvas was not allowed<br/>";
+    } else {
+        document.getElementById("console").innerHTML += "FAIL: Reading data from a canvas tainted by a tainted canvas was allowed<br/>";
+    }
+    
+    document.getElementById("log").appendChild(canvas);
+    
+    // Test reading after using a tainted pattern
+    canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var context = canvas.getContext("2d");
+    var remoteImagePattern = context.createPattern(image, "repeat");
+    context.fillStyle = remoteImagePattern;
+    context.fillRect(0,0,100,100);
+    if (context.getImageData(0,0,100,100) == null) {
+        document.getElementById("console").innerHTML += "PASS: Reading data from a canvas tainted by a remote image tainted pattern was not allowed<br/>";
+    } else {
+        document.getElementById("console").innerHTML += "FAIL: Reading data from a canvas tainted by a remote image tainted pattern was allowed<br/>";
+    }
+    
+    document.getElementById("log").appendChild(canvas);
+    
+    // Test reading after using a tainted pattern
+    canvas = document.createElement("canvas");
+    canvas.width = 100;
+    canvas.height = 100;
+    var context = canvas.getContext("2d");
+    var taintedCanvasPattern = context.createPattern(dirtyCanvas, "repeat");
+    context.fillStyle = taintedCanvasPattern;
+    context.fillRect(0,0,100,100);
+    if (context.getImageData(0,0,100,100) == null) {
+        document.getElementById("console").innerHTML += "PASS: Reading data from a canvas tainted by a tainted canvas pattern was not allowed<br/>";
+    } else {
+        document.getElementById("console").innerHTML += "FAIL: Reading data from a canvas tainted by a tainted canvas pattern was allowed<br/>";
+    }
+    
+    document.getElementById("log").appendChild(canvas);
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+image.src = "http://localhost:8000/security/resources/abe.png";
+</script>
index 3d4d4c8..063318b 100644 (file)
@@ -1,3 +1,36 @@
+2008-03-06  Sam Weinig  <sam@webkit.org> with a little help from Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Mitz.
+
+        Implement the HTML5 canvas tainting rules to prevent potential data leakage
+
+        Added originClean to HTMLCanvasElement and CanvasPattern
+        to track whether a canvas (or pattern) is tainted by remote
+        data.
+        Use originClean flag to determine whether getImageData should
+        return, well, image data.
+
+        Test: http/tests/security/canvas-remote-read-remote-image.html
+
+        * html/CanvasPattern.cpp:
+        (WebCore::CanvasPattern::CanvasPattern):
+        * html/CanvasPattern.h:
+        * html/CanvasRenderingContext2D.cpp:
+        (WebCore::CanvasRenderingContext2D::setStrokeStyle):
+        (WebCore::CanvasRenderingContext2D::setFillStyle):
+        (WebCore::CanvasRenderingContext2D::checkOrigin):
+        (WebCore::CanvasRenderingContext2D::drawImage):
+        (WebCore::CanvasRenderingContext2D::drawImageFromRect):
+        (WebCore::CanvasRenderingContext2D::createPattern):
+        (WebCore::CanvasRenderingContext2D::printSecurityExceptionMessage):
+        (WebCore::CanvasRenderingContext2D::getImageData):
+        * html/CanvasRenderingContext2D.h:
+        * html/HTMLCanvasElement.cpp:
+        (WebCore::HTMLCanvasElement::HTMLCanvasElement):
+        * html/HTMLCanvasElement.h:
+        (WebCore::HTMLCanvasElement::setOriginTainted):
+        (WebCore::HTMLCanvasElement::originClean):
+
 2008-03-06  Anders Carlsson  <andersca@apple.com>
 
         Reviewed by Jon.
index bc39d1a..2cda917 100644 (file)
@@ -65,29 +65,31 @@ void CanvasPattern::parseRepetitionType(const String& type, bool& repeatX, bool&
 
 #if PLATFORM(CG)
 
-CanvasPattern::CanvasPattern(CGImageRef image, bool repeatX, bool repeatY)
+CanvasPattern::CanvasPattern(CGImageRef image, bool repeatX, bool repeatY, bool originClean)
     : RefCounted<CanvasPattern>(0)
     , m_platformImage(image)
     , m_cachedImage(0)
     , m_repeatX(repeatX)
     , m_repeatY(repeatY)
+    , m_originClean(originClean)
 {
 }
 
 #elif PLATFORM(CAIRO)
 
-CanvasPattern::CanvasPattern(cairo_surface_t* surface, bool repeatX, bool repeatY)
+CanvasPattern::CanvasPattern(cairo_surface_t* surface, bool repeatX, bool repeatY, bool originClean)
     : RefCounted<CanvasPattern>(0)
     , m_platformImage(cairo_surface_reference(surface))
     , m_cachedImage(0)
     , m_repeatX(repeatX)
     , m_repeatY(repeatY)
+    , m_originClean(originClean)
 {
 }
 
 #endif
 
-CanvasPattern::CanvasPattern(CachedImage* cachedImage, bool repeatX, bool repeatY)
+CanvasPattern::CanvasPattern(CachedImage* cachedImage, bool repeatX, bool repeatY, bool originClean)
     : RefCounted<CanvasPattern>(0)
 #if PLATFORM(CG) || PLATFORM(CAIRO)
     , m_platformImage(0)
@@ -95,6 +97,7 @@ CanvasPattern::CanvasPattern(CachedImage* cachedImage, bool repeatX, bool repeat
     , m_cachedImage(cachedImage)
     , m_repeatX(repeatX)
     , m_repeatY(repeatY)
+    , m_originClean(originClean)
 {
     if (cachedImage)
         cachedImage->ref(this);
index 4990776..8678702 100644 (file)
@@ -48,11 +48,11 @@ namespace WebCore {
         static void parseRepetitionType(const String&, bool& repeatX, bool& repeatY, ExceptionCode&);
 
 #if PLATFORM(CG)
-        CanvasPattern(CGImageRef, bool repeatX, bool repeatY);
+        CanvasPattern(CGImageRef, bool repeatX, bool repeatY, bool originClean);
 #elif PLATFORM(CAIRO)
-        CanvasPattern(cairo_surface_t*, bool repeatX, bool repeatY);
+        CanvasPattern(cairo_surface_t*, bool repeatX, bool repeatY, bool originClean);
 #endif
-        CanvasPattern(CachedImage*, bool repeatX, bool repeatY);
+        CanvasPattern(CachedImage*, bool repeatX, bool repeatY, bool originClean);
         ~CanvasPattern();
 
 #if PLATFORM(CG)
@@ -68,6 +68,8 @@ namespace WebCore {
         cairo_pattern_t* createPattern(const cairo_matrix_t&);
 #endif
 
+        bool originClean() const { return m_originClean; }
+
     private:
 #if PLATFORM(CG)
         const RetainPtr<CGImageRef> m_platformImage;
@@ -77,6 +79,7 @@ namespace WebCore {
         CachedImage* const m_cachedImage;
         const bool m_repeatX;
         const bool m_repeatY;
+        bool m_originClean;
     };
 
 } // namespace WebCore
index 26509ac..db9390c 100644 (file)
 #include "HTMLNames.h"
 #include "ImageBuffer.h"
 #include "ImageData.h"
+#include "KURL.h"
 #include "NotImplemented.h"
+#include "Page.h"
 #include "RenderHTMLCanvas.h"
+#include "SecurityOrigin.h"
 #include "Settings.h"
+#include <kjs/interpreter.h>
 #include <wtf/MathExtras.h>
 
 #if PLATFORM(QT)
@@ -122,6 +126,14 @@ void CanvasRenderingContext2D::setStrokeStyle(PassRefPtr<CanvasStyle> style)
 {
     if (!style)
         return;
+
+    if (m_canvas->originClean()) {
+        if (CanvasPattern* pattern = style->pattern()) {
+            if (!pattern->originClean())
+                m_canvas->setOriginTainted();
+        }
+    }
+
     state().m_strokeStyle = style;
     GraphicsContext* c = drawingContext();
     if (!c)
@@ -139,6 +151,14 @@ void CanvasRenderingContext2D::setFillStyle(PassRefPtr<CanvasStyle> style)
 {
     if (!style)
         return;
+    if (m_canvas->originClean()) {
+        if (CanvasPattern* pattern = style->pattern()) {
+            if (!pattern->originClean())
+                m_canvas->setOriginTainted();
+        }
+    }
+
     state().m_fillStyle = style;
     GraphicsContext* c = drawingContext();
     if (!c)
@@ -881,6 +901,14 @@ void CanvasRenderingContext2D::drawImage(HTMLImageElement* image,
     drawImage(image, FloatRect(0, 0, s.width(), s.height()), FloatRect(x, y, width, height), ec);
 }
 
+void CanvasRenderingContext2D::checkOrigin(const KURL& url)
+{
+    RefPtr<SecurityOrigin> origin = SecurityOrigin::create(url.protocol(), url.host(), url.port(), 0);
+    SecurityOrigin::Reason reason;
+    if (!m_canvas->document()->securityOrigin()->canAccess(origin.get(), reason))
+        m_canvas->setOriginTainted();
+}
+
 void CanvasRenderingContext2D::drawImage(HTMLImageElement* image, const FloatRect& srcRect, const FloatRect& dstRect,
     ExceptionCode& ec)
 {
@@ -906,6 +934,9 @@ void CanvasRenderingContext2D::drawImage(HTMLImageElement* image, const FloatRec
     if (!cachedImage)
         return;
 
+    if (m_canvas->originClean())
+        checkOrigin(KURL(cachedImage->url()));
+
     FloatRect sourceRect = c->roundToDevicePixels(srcRect);
     FloatRect destRect = c->roundToDevicePixels(dstRect);
     willDraw(destRect);
@@ -954,7 +985,10 @@ void CanvasRenderingContext2D::drawImage(HTMLCanvasElement* canvas, const FloatR
     ImageBuffer* buffer = canvas->buffer();
     if (!buffer)
         return;
-    
+
+    if (!canvas->originClean())
+        m_canvas->setOriginTainted();
+
     willDraw(destRect);
     c->drawImage(buffer, sourceRect, destRect);
 }
@@ -972,6 +1006,9 @@ void CanvasRenderingContext2D::drawImageFromRect(HTMLImageElement* image,
     if (!cachedImage)
         return;
 
+    if (m_canvas->originClean())
+        checkOrigin(KURL(cachedImage->url()));
+
     GraphicsContext* c = drawingContext();
     if (!c)
         return;
@@ -1013,7 +1050,15 @@ PassRefPtr<CanvasPattern> CanvasRenderingContext2D::createPattern(HTMLImageEleme
     CanvasPattern::parseRepetitionType(repetitionType, repeatX, repeatY, ec);
     if (ec)
         return 0;
-    return new CanvasPattern(image ? image->cachedImage() : 0, repeatX, repeatY);
+    
+    bool originClean = true;
+    if (CachedImage* cachedImage = image->cachedImage()) {
+        KURL url(cachedImage->url());
+        RefPtr<SecurityOrigin> origin = SecurityOrigin::create(url.protocol(), url.host(), url.port(), 0);
+        SecurityOrigin::Reason reason;
+        originClean = m_canvas->document()->securityOrigin()->canAccess(origin.get(), reason);
+    }
+    return new CanvasPattern(image->cachedImage(), repeatX, repeatY, originClean);
 }
 
 PassRefPtr<CanvasPattern> CanvasRenderingContext2D::createPattern(HTMLCanvasElement* canvas,
@@ -1029,14 +1074,14 @@ PassRefPtr<CanvasPattern> CanvasRenderingContext2D::createPattern(HTMLCanvasElem
     CGImageRef image = canvas->createPlatformImage();
     if (!image)
         return 0;
-    PassRefPtr<CanvasPattern> pattern = new CanvasPattern(image, repeatX, repeatY);
+    PassRefPtr<CanvasPattern> pattern = new CanvasPattern(image, repeatX, repeatY, canvas->originClean());
     CGImageRelease(image);
     return pattern;
 #elif PLATFORM(CAIRO)
     cairo_surface_t* surface = canvas->createPlatformImage();
     if (!surface)
         return 0;
-    PassRefPtr<CanvasPattern> pattern = new CanvasPattern(surface, repeatX, repeatY);
+    PassRefPtr<CanvasPattern> pattern = new CanvasPattern(surface, repeatX, repeatY, canvas->originClean());
     cairo_surface_destroy(surface);
     return pattern;
 #else
@@ -1188,8 +1233,30 @@ PassRefPtr<ImageData> CanvasRenderingContext2D::createImageData(float sw, float
     return createEmptyImageData(scaledSize);
 }
 
+void CanvasRenderingContext2D::printSecurityExceptionMessage() const
+{
+    static const char* message = "Call to getImageData failed due to tainted canvas.\n";
+
+    Frame* frame = m_canvas->document()->frame();
+    if (!frame)
+        return;
+    if (frame->settings()->privateBrowsingEnabled())
+        return;
+    if (KJS::Interpreter::shouldPrintExceptions())
+        printf("%s", message);
+    if (Page* page = frame->page())
+        page->chrome()->addMessageToConsole(JSMessageSource, ErrorMessageLevel, message, 1, String()); // FIXME: provide a real line number and source URL.
+}
+
 PassRefPtr<ImageData> CanvasRenderingContext2D::getImageData(float sx, float sy, float sw, float sh) const
 {
+    if (!m_canvas->originClean()) {
+        // FIXME: the WHATWG specification says that this should raise a "security exception", but does not currently
+        // define what one is.  For now, we will silently fail with only a log message.
+        printSecurityExceptionMessage();
+        return 0;
+    }
+    
     FloatRect unscaledRect(sx, sy, sw, sh);
     IntRect scaledRect = m_canvas ? m_canvas->convertLogicalToDevice(unscaledRect) : enclosingIntRect(unscaledRect);
     if (scaledRect.width() < 1)
index 5f79d53..acb75aa 100644 (file)
@@ -46,6 +46,7 @@ namespace WebCore {
     class HTMLCanvasElement;
     class HTMLImageElement;
     class ImageData;
+    class KURL;
 
     typedef int ExceptionCode;
 
@@ -210,6 +211,10 @@ namespace WebCore {
 
         void clearPathForDashboardBackwardCompatibilityMode();
 
+        void checkOrigin(const KURL&);
+
+        void printSecurityExceptionMessage() const;
+
         HTMLCanvasElement* m_canvas;
         Vector<State, 1> m_stateStack;
     };
index 45895c7..5444cbf 100644 (file)
@@ -65,6 +65,7 @@ const float HTMLCanvasElement::MaxCanvasArea = 32768 * 8192; // Maximum canvas a
 HTMLCanvasElement::HTMLCanvasElement(Document* doc)
     : HTMLElement(canvasTag, doc)
     , m_size(defaultWidth, defaultHeight)
+    , m_originClean(true)
     , m_createdImageBuffer(false)
 {
 }
index 2876be3..4fbc7f6 100644 (file)
@@ -92,7 +92,12 @@ public:
     IntRect convertLogicalToDevice(const FloatRect&) const;
     IntSize convertLogicalToDevice(const FloatSize&) const;
     IntPoint convertLogicalToDevice(const FloatPoint&) const;
+
+    void setOriginTainted() { m_originClean = false; } 
+    bool originClean() const { return m_originClean; }
+
     static const float MaxCanvasArea;
+
 private:
     void createImageBuffer() const;
     void reset();
@@ -102,8 +107,7 @@ private:
     RefPtr<CanvasRenderingContext2D> m_2DContext;
     IntSize m_size;
 
-    // FIXME: Web Applications 1.0 describes a security feature where we track
-    // if we ever drew any images outside the domain, so we can disable toDataURL.
+    bool m_originClean;
 
     // m_createdImageBuffer means we tried to malloc the buffer.  We didn't necessarily get it.
     mutable bool m_createdImageBuffer;