HLS resources with remote subresources will not taint canvasses.
authorjer.noble@apple.com <jer.noble@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Jul 2018 17:59:47 +0000 (17:59 +0000)
committerjer.noble@apple.com <jer.noble@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Jul 2018 17:59:47 +0000 (17:59 +0000)
https://bugs.webkit.org/show_bug.cgi?id=187731
<rdar://problem/42290703>

Reviewed by Brady Eidson.

Source/WebCore:

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

Most media sources are single-resource; they are accessed from a single origin. HLS manifests can contain many
subresources from arbitrary origins, and canvases should be tainted when painted from media elements whose
subresources were retrieved from tainting origins.

Add a new method to HTMLMediaElement, wouldTaintOrigin(), taking a SecurityOrigin, and returning whether the
media element would taint that origin. This gets piped all the way down to MediaPlayerPrivateAVFoundationObjC
which uses WebCoreNSURLSession to track all the origins of all the responses which resulted from the media
element's load.

Drive-by fix: also fix this issue for media elements which render to an AudioContext.

Drive-by fix #2: CanvasRenderingContext2DBase::createPattern() needs to check the return value of
ImageBuffer::create() before using it.

* Modules/webaudio/MediaElementAudioSourceNode.cpp:
(WebCore::MediaElementAudioSourceNode::wouldTaintOrigin):
* html/HTMLMediaElement.cpp:
(WebCore::HTMLMediaElement::didAttachRenderers):
(WebCore::HTMLMediaElement::didDetachRenderers):
(WebCore::HTMLMediaElement::scheduleUpdateShouldAutoplay):
* html/HTMLMediaElement.h:
(WebCore::HTMLMediaElement::wouldTaintOrigin const):
* html/canvas/CanvasRenderingContext.cpp:
(WebCore::CanvasRenderingContext::wouldTaintOrigin):
* html/canvas/CanvasRenderingContext2DBase.cpp:
(WebCore::CanvasRenderingContext2DBase::createPattern):
* platform/graphics/MediaPlayer.cpp:
(WebCore::MediaPlayer::wouldTaintOrigin const):
* platform/graphics/MediaPlayer.h:
* platform/graphics/MediaPlayerPrivate.h:
(WebCore::MediaPlayerPrivateInterface::hasSingleSecurityOrigin const):
(WebCore::MediaPlayerPrivateInterface::wouldTaintOrigin const):
* platform/graphics/avfoundation/objc/CDMSessionAVContentKeySession.mm:
(WebCore::CDMSessionAVContentKeySession::update):
* platform/graphics/avfoundation/objc/MediaPlayerPrivateAVFoundationObjC.h:
* platform/graphics/avfoundation/objc/MediaPlayerPrivateAVFoundationObjC.mm:
(WebCore::MediaPlayerPrivateAVFoundationObjC::wouldTaintOrigin const):
* platform/network/cocoa/WebCoreNSURLSession.h:
* platform/network/cocoa/WebCoreNSURLSession.mm:
(-[WebCoreNSURLSession task:didReceiveResponseFromOrigin:]):
(-[WebCoreNSURLSession wouldTaintOrigin:]):
(-[WebCoreNSURLSessionDataTask resource:receivedResponse:]):

LayoutTests:

* http/tests/media/resources/hls/test-vod-localhost.m3u8: Added.
* http/tests/security/canvas-remote-read-remote-video-hls-expected.txt: Added.
* http/tests/security/canvas-remote-read-remote-video-hls.html: Added.

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

17 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/media/resources/hls/test-vod-localhost.m3u8 [new file with mode: 0644]
LayoutTests/http/tests/security/canvas-remote-read-remote-video-hls-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/security/canvas-remote-read-remote-video-hls.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/Modules/webaudio/MediaElementAudioSourceNode.cpp
Source/WebCore/html/HTMLMediaElement.h
Source/WebCore/html/canvas/CanvasRenderingContext.cpp
Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp
Source/WebCore/platform/graphics/MediaPlayer.cpp
Source/WebCore/platform/graphics/MediaPlayer.h
Source/WebCore/platform/graphics/MediaPlayerPrivate.h
Source/WebCore/platform/graphics/avfoundation/objc/CDMSessionAVContentKeySession.mm
Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateAVFoundationObjC.h
Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateAVFoundationObjC.mm
Source/WebCore/platform/network/cocoa/WebCoreNSURLSession.h
Source/WebCore/platform/network/cocoa/WebCoreNSURLSession.mm

index 79facf2..14d83d3 100644 (file)
@@ -1,3 +1,15 @@
+2018-07-19  Jer Noble  <jer.noble@apple.com>
+
+        HLS resources with remote subresources will not taint canvasses.
+        https://bugs.webkit.org/show_bug.cgi?id=187731
+        <rdar://problem/42290703>
+
+        Reviewed by Brady Eidson.
+
+        * http/tests/media/resources/hls/test-vod-localhost.m3u8: Added.
+        * http/tests/security/canvas-remote-read-remote-video-hls-expected.txt: Added.
+        * http/tests/security/canvas-remote-read-remote-video-hls.html: Added.
+
 2018-07-20  Ryan Haddad  <ryanhaddad@apple.com>
 
         Rebaseline editing/mac/attributed-string/attributed-string-for-typing-with-color-filter.html for Sierra.
diff --git a/LayoutTests/http/tests/media/resources/hls/test-vod-localhost.m3u8 b/LayoutTests/http/tests/media/resources/hls/test-vod-localhost.m3u8
new file mode 100644 (file)
index 0000000..cdc46f8
--- /dev/null
@@ -0,0 +1,8 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:6
+#EXT-X-VERSION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:6.0272,
+http://localhost:8000/media/resources/hls/test.ts
+#EXT-X-ENDLIST
diff --git a/LayoutTests/http/tests/security/canvas-remote-read-remote-video-hls-expected.txt b/LayoutTests/http/tests/security/canvas-remote-read-remote-video-hls-expected.txt
new file mode 100644 (file)
index 0000000..fbadaa0
--- /dev/null
@@ -0,0 +1,17 @@
+CONSOLE MESSAGE: line 1: Unable to get image data from canvas because the canvas has been tainted by cross-origin data.
+Ensure that data cannot be retrieved from a canvas tainted by a video resource with remote subresources when CORS is not enabled.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Testing data retrieval on an untainted canvas:
+PASS canvas.getContext('2d').getImageData(0, 0, 100, 100) did not throw exception.
+PASS canvas.toDataURL() did not throw exception.
+
+Testing data retrieval on a canvas tainted by a pattern generated by a hls video resource containing remote contents:
+PASS context.getImageData(0, 0, 100, 100) threw exception SecurityError: The operation is insecure..
+PASS canvas.toDataURL() threw exception SecurityError: The operation is insecure..
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/canvas-remote-read-remote-video-hls.html b/LayoutTests/http/tests/security/canvas-remote-read-remote-video-hls.html
new file mode 100644 (file)
index 0000000..329631c
--- /dev/null
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="../../../resources/js-test-pre.js"></script>
+    <script src="../../media-resources/media-file.js"></script>
+    <script src="resources/canvas-video-crossorigin.js"></script>
+</head>
+<body>
+<pre id="console"></pre>
+<script>
+    description("Ensure that data cannot be retrieved from a canvas tainted by a video resource with remote subresources when CORS is not enabled.");
+
+    function test()
+    {
+        testDataRetrievalForbidden("hls video resource containing remote contents");
+        finishJSTest();
+    }
+
+    var video = document.createElement("video");
+    video.addEventListener("loadeddata", test);
+    video.src = '/media/resources/hls/test-vod-localhost.m3u8'
+    
+    window.jsTestIsAsync = true;
+</script>
+<script src="../../../resources/js-test-post.js"></script>
+</body>
+</html>
index 2e6b1a9..485ee93 100644 (file)
@@ -1,3 +1,56 @@
+2018-07-19  Jer Noble  <jer.noble@apple.com>
+
+        HLS resources with remote subresources will not taint canvasses.
+        https://bugs.webkit.org/show_bug.cgi?id=187731
+        <rdar://problem/42290703>
+
+        Reviewed by Brady Eidson.
+
+        Test: http/tests/security/canvas-remote-read-remote-video-hls.html
+
+        Most media sources are single-resource; they are accessed from a single origin. HLS manifests can contain many
+        subresources from arbitrary origins, and canvases should be tainted when painted from media elements whose
+        subresources were retrieved from tainting origins.
+
+        Add a new method to HTMLMediaElement, wouldTaintOrigin(), taking a SecurityOrigin, and returning whether the
+        media element would taint that origin. This gets piped all the way down to MediaPlayerPrivateAVFoundationObjC
+        which uses WebCoreNSURLSession to track all the origins of all the responses which resulted from the media
+        element's load.
+
+        Drive-by fix: also fix this issue for media elements which render to an AudioContext.
+
+        Drive-by fix #2: CanvasRenderingContext2DBase::createPattern() needs to check the return value of
+        ImageBuffer::create() before using it.
+
+        * Modules/webaudio/MediaElementAudioSourceNode.cpp:
+        (WebCore::MediaElementAudioSourceNode::wouldTaintOrigin):
+        * html/HTMLMediaElement.cpp:
+        (WebCore::HTMLMediaElement::didAttachRenderers):
+        (WebCore::HTMLMediaElement::didDetachRenderers):
+        (WebCore::HTMLMediaElement::scheduleUpdateShouldAutoplay):
+        * html/HTMLMediaElement.h:
+        (WebCore::HTMLMediaElement::wouldTaintOrigin const):
+        * html/canvas/CanvasRenderingContext.cpp:
+        (WebCore::CanvasRenderingContext::wouldTaintOrigin):
+        * html/canvas/CanvasRenderingContext2DBase.cpp:
+        (WebCore::CanvasRenderingContext2DBase::createPattern):
+        * platform/graphics/MediaPlayer.cpp:
+        (WebCore::MediaPlayer::wouldTaintOrigin const):
+        * platform/graphics/MediaPlayer.h:
+        * platform/graphics/MediaPlayerPrivate.h:
+        (WebCore::MediaPlayerPrivateInterface::hasSingleSecurityOrigin const):
+        (WebCore::MediaPlayerPrivateInterface::wouldTaintOrigin const):
+        * platform/graphics/avfoundation/objc/CDMSessionAVContentKeySession.mm:
+        (WebCore::CDMSessionAVContentKeySession::update):
+        * platform/graphics/avfoundation/objc/MediaPlayerPrivateAVFoundationObjC.h:
+        * platform/graphics/avfoundation/objc/MediaPlayerPrivateAVFoundationObjC.mm:
+        (WebCore::MediaPlayerPrivateAVFoundationObjC::wouldTaintOrigin const):
+        * platform/network/cocoa/WebCoreNSURLSession.h:
+        * platform/network/cocoa/WebCoreNSURLSession.mm:
+        (-[WebCoreNSURLSession task:didReceiveResponseFromOrigin:]):
+        (-[WebCoreNSURLSession wouldTaintOrigin:]):
+        (-[WebCoreNSURLSessionDataTask resource:receivedResponse:]):
+
 2018-07-20  Zalan Bujtas  <zalan@apple.com>
 
         Update FrameView::paintContents to use release logging.
index 9eca86f..576554d 100644 (file)
@@ -110,7 +110,12 @@ bool MediaElementAudioSourceNode::wouldTaintOrigin()
     if (m_mediaElement->didPassCORSAccessCheck())
         return false;
 
-    return context().wouldTaintOrigin(m_mediaElement->currentSrc());
+    if (auto* scriptExecutionContext = context().scriptExecutionContext()) {
+        if (auto* origin = scriptExecutionContext->securityOrigin())
+            return m_mediaElement->wouldTaintOrigin(*origin);
+    }
+
+    return true;
 }
 
 void MediaElementAudioSourceNode::process(size_t numberOfFrames)
index d53a8b6..cd61000 100644 (file)
@@ -415,6 +415,7 @@ public:
 
     bool hasSingleSecurityOrigin() const { return !m_player || m_player->hasSingleSecurityOrigin(); }
     bool didPassCORSAccessCheck() const { return m_player && m_player->didPassCORSAccessCheck(); }
+    bool wouldTaintOrigin(const SecurityOrigin& origin) const { return m_player && m_player->wouldTaintOrigin(origin); }
     
     WEBCORE_EXPORT bool isFullscreen() const override;
     bool isStandardFullscreen() const;
index 8582a06..c346102 100644 (file)
@@ -109,7 +109,7 @@ bool CanvasRenderingContext::wouldTaintOrigin(const HTMLVideoElement* video)
     if (!video->hasSingleSecurityOrigin())
         return true;
 
-    if (!(video->player() && video->player()->didPassCORSAccessCheck()) && wouldTaintOrigin(video->currentSrc()))
+    if (!(video->player() && video->player()->didPassCORSAccessCheck()) && video->wouldTaintOrigin(*m_canvas.securityOrigin()))
         return true;
 
 #else
index cd6f3ef..6202ea5 100644 (file)
@@ -1970,6 +1970,9 @@ ExceptionOr<RefPtr<CanvasPattern>> CanvasRenderingContext2DBase::createPattern(H
 #endif
 
     auto imageBuffer = ImageBuffer::create(size(videoElement), drawingContext() ? drawingContext()->renderingMode() : Accelerated);
+    if (!imageBuffer)
+        return nullptr;
+
     videoElement.paintCurrentFrameInContext(imageBuffer->context(), FloatRect(FloatPoint(), size(videoElement)));
     
     return RefPtr<CanvasPattern> { CanvasPattern::create(ImageBuffer::sinkIntoImage(WTFMove(imageBuffer), PreserveResolution::Yes).releaseNonNull(), repeatX, repeatY, originClean) };
index 2f91600..c3af392 100644 (file)
@@ -1046,6 +1046,18 @@ bool MediaPlayer::didPassCORSAccessCheck() const
     return m_private->didPassCORSAccessCheck();
 }
 
+bool MediaPlayer::wouldTaintOrigin(const SecurityOrigin& origin) const
+{
+    auto wouldTaint = m_private->wouldTaintOrigin(origin);
+    if (wouldTaint.has_value())
+        return wouldTaint.value();
+
+    if (m_url.protocolIsData())
+        return true;
+
+    return origin.canRequest(m_url);
+}
+
 MediaPlayer::MovieLoadType MediaPlayer::movieLoadType() const
 {
     return m_private->movieLoadType();
index 2de9580..97c68e2 100644 (file)
@@ -461,8 +461,8 @@ public:
 #endif
 
     bool hasSingleSecurityOrigin() const;
-
     bool didPassCORSAccessCheck() const;
+    bool wouldTaintOrigin(const SecurityOrigin&) const;
 
     MediaTime mediaTimeForTimeValue(const MediaTime&) const;
 
index abd24f3..54547f9 100644 (file)
@@ -189,8 +189,8 @@ public:
     virtual void setShouldMaintainAspectRatio(bool) { }
 
     virtual bool hasSingleSecurityOrigin() const { return false; }
-
     virtual bool didPassCORSAccessCheck() const { return false; }
+    virtual std::optional<bool> wouldTaintOrigin(const SecurityOrigin&) const { return std::nullopt; }
 
     virtual MediaPlayer::MovieLoadType movieLoadType() const { return MediaPlayer::Unknown; }
 
index 9856226..5edac46 100644 (file)
@@ -237,18 +237,6 @@ bool CDMSessionAVContentKeySession::update(Uint8Array* key, RefPtr<Uint8Array>&
 {
     UNUSED_PARAM(nextMessage);
 
-    if (m_stopped) {
-        errorCode = MediaPlayer::InvalidPlayerState;
-        return false;
-    }
-
-    bool shouldGenerateKeyRequest = !m_certificate || isEqual(key, "renew");
-    if (!m_certificate) {
-        LOG(Media, "CDMSessionAVContentKeySession::update(%p) - certificate data", this);
-
-        m_certificate = key;
-    }
-
     if (isEqual(key, "acknowledged")) {
         LOG(Media, "CDMSessionAVContentKeySession::update(%p) - acknowleding secure stop message", this);
 
@@ -266,6 +254,18 @@ bool CDMSessionAVContentKeySession::update(Uint8Array* key, RefPtr<Uint8Array>&
         return true;
     }
 
+    if (m_stopped) {
+        errorCode = MediaPlayer::InvalidPlayerState;
+        return false;
+    }
+
+    bool shouldGenerateKeyRequest = !m_certificate || isEqual(key, "renew");
+    if (!m_certificate) {
+        LOG(Media, "CDMSessionAVContentKeySession::update(%p) - certificate data", this);
+
+        m_certificate = key;
+    }
+
     if (m_mode == KeyRelease)
         return false;
 
index 38325e5..ab2f246 100644 (file)
@@ -227,6 +227,8 @@ private:
     void updateVideoLayerGravity() override;
 
     bool didPassCORSAccessCheck() const override;
+    std::optional<bool> wouldTaintOrigin(const SecurityOrigin&) const final;
+
 
     MediaTime getStartDate() const override;
 
index 6125797..f907c7f 100644 (file)
@@ -2219,6 +2219,22 @@ bool MediaPlayerPrivateAVFoundationObjC::didPassCORSAccessCheck() const
     return false;
 }
 
+std::optional<bool> MediaPlayerPrivateAVFoundationObjC::wouldTaintOrigin(const SecurityOrigin& origin) const
+{
+#if PLATFORM(IOS) || __MAC_OS_X_VERSION_MIN_REQUIRED > 101100
+    AVAssetResourceLoader *resourceLoader = m_avAsset.get().resourceLoader;
+    if (!DeprecatedGlobalSettings::isAVFoundationNSURLSessionEnabled()
+        || ![resourceLoader respondsToSelector:@selector(URLSession)])
+        return false;
+
+    WebCoreNSURLSession *session = (WebCoreNSURLSession *)resourceLoader.URLSession;
+    if ([session isKindOfClass:[WebCoreNSURLSession class]])
+        return [session wouldTaintOrigin:origin];
+#endif
+    return std::nullopt;
+}
+
+
 #if HAVE(AVFOUNDATION_VIDEO_OUTPUT)
 
 void MediaPlayerPrivateAVFoundationObjC::createVideoOutput()
index 9873bc3..f42ac03 100644 (file)
@@ -26,6 +26,7 @@
 #ifndef WebCoreNSURLSession_h
 #define WebCoreNSURLSession_h
 
+#import "SecurityOrigin.h"
 #import <Foundation/NSURLSession.h>
 #import <wtf/HashSet.h>
 #import <wtf/Lock.h>
@@ -62,6 +63,7 @@ WEBCORE_EXPORT @interface WebCoreNSURLSession : NSObject {
     RetainPtr<NSOperationQueue> _queue;
     NSString *_sessionDescription;
     HashSet<RetainPtr<WebCoreNSURLSessionDataTask>> _dataTasks;
+    HashSet<RefPtr<WebCore::SecurityOrigin>> _origins;
     Lock _dataTasksLock;
     BOOL _invalidated;
     NSUInteger _nextTaskIdentifier;
@@ -76,6 +78,7 @@ WEBCORE_EXPORT @interface WebCoreNSURLSession : NSObject {
 @property (readonly) BOOL didPassCORSAccessChecks;
 - (void)finishTasksAndInvalidate;
 - (void)invalidateAndCancel;
+- (BOOL)wouldTaintOrigin:(const WebCore::SecurityOrigin&)origin;
 
 - (void)resetWithCompletionHandler:(void (^)(void))completionHandler;
 - (void)flushWithCompletionHandler:(void (^)(void))completionHandler;
index 8905013..de921f1 100644 (file)
@@ -44,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)taskCompleted:(WebCoreNSURLSessionDataTask *)task;
 - (void)addDelegateOperation:(Function<void()>&&)operation;
 - (void)task:(WebCoreNSURLSessionDataTask *)task didReceiveCORSAccessCheckResult:(BOOL)result;
+- (void)task:(WebCoreNSURLSessionDataTask *)task didReceiveResponseFromOrigin:(Ref<WebCore::SecurityOrigin>&&)origin;
 @end
 
 @interface WebCoreNSURLSessionDataTask ()
@@ -147,6 +148,12 @@ NS_ASSUME_NONNULL_END
         _corsResults = WebCoreNSURLSessionCORSAccessCheckResults::Pass;
 }
 
+- (void)task:(WebCoreNSURLSessionDataTask *)task didReceiveResponseFromOrigin:(Ref<WebCore::SecurityOrigin>&&)origin
+{
+    UNUSED_PARAM(task);
+    _origins.add(WTFMove(origin));
+}
+
 #pragma mark - NSURLSession API
 @synthesize sessionDescription=_sessionDescription;
 @dynamic delegate;
@@ -184,6 +191,15 @@ NS_ASSUME_NONNULL_END
     return _corsResults == WebCoreNSURLSessionCORSAccessCheckResults::Pass;
 }
 
+- (BOOL)wouldTaintOrigin:(const WebCore::SecurityOrigin &)origin
+{
+    for (auto& responseOrigin : _origins) {
+        if (!origin.canAccess(*responseOrigin))
+            return true;
+    }
+    return false;
+}
+
 - (void)finishTasksAndInvalidate
 {
     _invalidated = YES;
@@ -614,6 +630,7 @@ void WebCoreNSURLSessionDataTaskClient::loadFinished(PlatformMediaResource& reso
     ASSERT(response.source() == ResourceResponse::Source::Network || response.source() == ResourceResponse::Source::DiskCache || response.source() == ResourceResponse::Source::DiskCacheAfterValidation || response.source() == ResourceResponse::Source::ServiceWorker);
     ASSERT_UNUSED(resource, &resource == _resource);
     ASSERT(isMainThread());
+    [self.session task:self didReceiveResponseFromOrigin:SecurityOrigin::create(response.url())];
     [self.session task:self didReceiveCORSAccessCheckResult:resource.didPassAccessControlCheck()];
     self.countOfBytesExpectedToReceive = response.expectedContentLength();
     [self _setDefersLoading:YES];