defeat cors attacks on audio/video tags

Neutralize error messages and fire no progress events
until media metadata has been loaded for media loaded
from cross-origin locations.

Bug: 828265, 826187
Change-Id: Iaf15ef38676403687d6a913cbdc84f2d70a6f5c6
Reviewed-on: https://chromium-review.googlesource.com/1015794
Reviewed-by: Mounir Lamouri <mlamouri@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Fredrik Hubinette <hubbe@chromium.org>
Cr-Commit-Position: refs/heads/master@{#557312}
diff --git a/third_party/WebKit/LayoutTests/http/tests/media/media-load-nonmedia-crossorigin.html b/third_party/WebKit/LayoutTests/http/tests/media/media-load-nonmedia-crossorigin.html
new file mode 100644
index 0000000..cd07bf7
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/http/tests/media/media-load-nonmedia-crossorigin.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<title>Check that crossorigin media requests don't reveal information about non-media files.</title>
+<script src="/w3c/resources/testharness.js"></script>
+<script src="/w3c/resources/testharnessreport.js"></script>
+<script src="/media-resources/media-file.js"></script>
+<video></video>
+<script>
+
+promise_test(async function() {
+  function get_all_events(url) {
+    return new Promise(function(resolve, reject) {
+      let events = [];
+      let current_state = -1;
+      let current_buffered_string = "";
+      const video = document.querySelector("video");
+      function pollTagState(prefix) {
+        const state = video.networkState;
+        if (state != current_state) {
+          current_state = state
+          events.push(prefix + "NetworkState=" + state);
+        }
+        const buffered = video.buffered;
+        let buffered_string = "";
+        for (let i = 0; i < buffered.length; i++) {
+          buffered_string += "(" + buffered.start(i) + "-" + buffered.end(i) + ")";
+        }
+	if (buffered_string != current_buffered_string) {
+          current_buffered_string = buffered_string;
+          events.push(prefix + "Buffered=" + buffered_string);
+	}
+      }
+
+      for (var prop in video) {
+        if (prop.slice(0,2) == "on") {
+          video[prop] = function(e) {
+            events.push(e.type);
+            pollTagState("+")
+          }
+        }
+      }
+      pollTagState("");
+      const interval = setInterval(function() { pollTagState("") }, 1);
+      video.onerror = function(e) {
+        events.push("Error("+video.error.message+")");
+        pollTagState("+")
+        // Wait for network state to stabilize.
+        setTimeout(function() {
+          clearInterval(interval);
+          resolve(events);
+        }, 100);
+      };
+      video.src = url;
+      video.play().catch(e=>0);
+    });
+  }
+
+  const nonexistant_remote = "http://localhost:8000/media/nonexistant.cgi";
+  const existant_remote = "http://localhost:8000/media/video-throttled-load.cgi?name=resources/test.txt&throttle=200&type=text/plain";
+  // First do a warmup run. Switching between sources adds some events, so
+  // the first run will be slightly different.
+  await get_all_events(nonexistant_remote);
+
+  // Get events for a nonexistant remote resource.
+  const nonexisting_events = await get_all_events(nonexistant_remote);
+
+  // Get events for a existant remote resource.
+  const existing_events = await get_all_events(existant_remote);
+
+  console.log(existing_events.join(","));
+  console.log(nonexisting_events.join(","));
+  assert_equals(existing_events.join(","), nonexisting_events.join(","));
+});
+
+</script>
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.cc b/third_party/blink/renderer/core/html/media/html_media_element.cc
index d8ee5ccf..10de9c2 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.cc
+++ b/third_party/blink/renderer/core/html/media/html_media_element.cc
@@ -1532,14 +1532,17 @@
     GetLayoutObject()->UpdateFromElement();
 }
 
-void HTMLMediaElement::NoneSupported(const String& message) {
-  BLINK_MEDIA_LOG << "NoneSupported(" << (void*)this << ", message='" << message
-                  << "')";
+void HTMLMediaElement::NoneSupported(const String& input_message) {
+  BLINK_MEDIA_LOG << "NoneSupported(" << (void*)this << ", message='"
+                  << input_message << "')";
 
   StopPeriodicTimers();
   load_state_ = kWaitingForSource;
   current_source_node_ = nullptr;
 
+  String empty_string;
+  const String& message = MediaShouldBeOpaque() ? empty_string : input_message;
+
   // 4.8.12.5
   // The dedicated media source failure steps are the following steps:
 
@@ -1615,11 +1618,17 @@
 }
 
 void HTMLMediaElement::MediaLoadingFailed(WebMediaPlayer::NetworkState error,
-                                          const String& message) {
+                                          const String& input_message) {
   BLINK_MEDIA_LOG << "MediaLoadingFailed(" << (void*)this << ", "
-                  << static_cast<int>(error) << ", message='" << message
+                  << static_cast<int>(error) << ", message='" << input_message
                   << "')";
 
+  bool should_be_opaque = MediaShouldBeOpaque();
+  if (should_be_opaque)
+    error = WebMediaPlayer::kNetworkStateNetworkError;
+  String empty_string;
+  const String& message = should_be_opaque ? empty_string : input_message;
+
   StopPeriodicTimers();
 
   // If we failed while trying to load a <source> element, the movie was never
@@ -1722,12 +1731,14 @@
 void HTMLMediaElement::ChangeNetworkStateFromLoadingToIdle() {
   progress_event_timer_.Stop();
 
-  // Schedule one last progress event so we guarantee that at least one is fired
-  // for files that load very quickly.
-  if (GetWebMediaPlayer() && GetWebMediaPlayer()->DidLoadingProgress())
-    ScheduleEvent(EventTypeNames::progress);
-  ScheduleEvent(EventTypeNames::suspend);
-  SetNetworkState(kNetworkIdle);
+  if (!MediaShouldBeOpaque()) {
+    // Schedule one last progress event so we guarantee that at least one is
+    // fired for files that load very quickly.
+    if (GetWebMediaPlayer() && GetWebMediaPlayer()->DidLoadingProgress())
+      ScheduleEvent(EventTypeNames::progress);
+    ScheduleEvent(EventTypeNames::suspend);
+    SetNetworkState(kNetworkIdle);
+  }
 }
 
 void HTMLMediaElement::ReadyStateChanged() {
@@ -1895,6 +1906,13 @@
   if (network_state_ != kNetworkLoading)
     return;
 
+  // If this is an cross-origin request, and we haven't discovered whether
+  // the media is actually playable yet, don't fire any progress events as
+  // those may let the page know information about the resource that it's
+  // not supposed to know.
+  if (MediaShouldBeOpaque())
+    return;
+
   double time = WTF::CurrentTime();
   double timedelta = time - previous_progress_time_;
 
@@ -4239,6 +4257,11 @@
   return autoplay_policy_->WasAutoplayInitiated();
 }
 
+bool HTMLMediaElement::MediaShouldBeOpaque() const {
+  return !IsMediaDataCORSSameOrigin(GetDocument().GetSecurityOrigin()) &&
+         ready_state_ < kHaveMetadata && !FastGetAttribute(srcAttr).IsEmpty();
+}
+
 void HTMLMediaElement::CheckViewportIntersectionTimerFired(TimerBase*) {
   bool should_report_root_bounds = true;
   IntersectionGeometry geometry(nullptr, *this, Vector<Length>(),
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.h b/third_party/blink/renderer/core/html/media/html_media_element.h
index f87d77f..ac00345 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.h
+++ b/third_party/blink/renderer/core/html/media/html_media_element.h
@@ -263,8 +263,8 @@
   using HTMLElement::GetExecutionContext;
 
   bool HasSingleSecurityOrigin() const {
-    return GetWebMediaPlayer() &&
-           GetWebMediaPlayer()->HasSingleSecurityOrigin();
+    return GetWebMediaPlayer() ? GetWebMediaPlayer()->HasSingleSecurityOrigin()
+                               : true;
   }
 
   bool IsFullscreen() const;
@@ -347,6 +347,11 @@
   InsertionNotificationRequest InsertedInto(ContainerNode*) override;
   void RemovedFrom(ContainerNode*) override;
 
+  // Return true if media is cross origin from the current document
+  // and has not passed a cors check, meaning that we should return
+  // as little information as possible about it.
+  bool MediaShouldBeOpaque() const;
+
   void DidMoveToNewDocument(Document& old_document) override;
   virtual KURL PosterImageURL() const { return KURL(); }