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(); }