Indexed DB: Correctly dispatch events from script

Events dispatched via dispatchEvent() in script should not trigger
side effects in Indexed DB objects. For untrusted events, propagate
correctly but otherwise early-exit from the dispatch functions.

Bug: 1032890
Change-Id: If4057ad2820419ef363e8e5f21670b3565946388
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1983272
Commit-Queue: Daniel Murphy <[email protected]>
Reviewed-by: Daniel Murphy <[email protected]>
Cr-Commit-Position: refs/heads/master@{#728285}
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_database.cc b/third_party/blink/renderer/modules/indexeddb/idb_database.cc
index 97f5b50..5cd42e5b 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_database.cc
+++ b/third_party/blink/renderer/modules/indexeddb/idb_database.cc
@@ -521,11 +521,18 @@
 
 DispatchEventResult IDBDatabase::DispatchEventInternal(Event& event) {
   IDB_TRACE("IDBDatabase::dispatchEvent");
-  if (!GetExecutionContext())
-    return DispatchEventResult::kCanceledBeforeDispatch;
+
+  event.SetTarget(this);
+
+  // If this event originated from script, it should have no side effects.
+  if (!event.isTrusted())
+    return EventTarget::DispatchEventInternal(event);
   DCHECK(event.type() == event_type_names::kVersionchange ||
          event.type() == event_type_names::kClose);
 
+  if (!GetExecutionContext())
+    return DispatchEventResult::kCanceledBeforeDispatch;
+
   DispatchEventResult dispatch_result =
       EventTarget::DispatchEventInternal(event);
   if (event.type() == event_type_names::kVersionchange && !close_pending_ &&
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_open_db_request.cc b/third_party/blink/renderer/modules/indexeddb/idb_open_db_request.cc
index d342cc5..44e60df 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_open_db_request.cc
+++ b/third_party/blink/renderer/modules/indexeddb/idb_open_db_request.cc
@@ -180,6 +180,15 @@
 }
 
 DispatchEventResult IDBOpenDBRequest::DispatchEventInternal(Event& event) {
+  // If this event originated from script, it should have no side effects.
+  if (!event.isTrusted())
+    return IDBRequest::DispatchEventInternal(event);
+  DCHECK(event.type() == event_type_names::kSuccess ||
+         event.type() == event_type_names::kError ||
+         event.type() == event_type_names::kBlocked ||
+         event.type() == event_type_names::kUpgradeneeded)
+      << "event type was " << event.type();
+
   // If the connection closed between onUpgradeNeeded and the delivery of the
   // "success" event, an "error" event should be fired instead.
   if (event.type() == event_type_names::kSuccess &&
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_request.cc b/third_party/blink/renderer/modules/indexeddb/idb_request.cc
index cb73df4..59dd47d 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_request.cc
+++ b/third_party/blink/renderer/modules/indexeddb/idb_request.cc
@@ -625,6 +625,29 @@
 
 DispatchEventResult IDBRequest::DispatchEventInternal(Event& event) {
   IDB_TRACE("IDBRequest::dispatchEvent");
+
+  event.SetTarget(this);
+
+  HeapVector<Member<EventTarget>> targets;
+  targets.push_back(this);
+  if (transaction_ && !prevent_propagation_) {
+    // Per spec: "A request's get the parent algorithm returns the request’s
+    // transaction."
+    targets.push_back(transaction_);
+    // Per spec: "A transaction's get the parent algorithm returns the
+    // transaction’s connection."
+    targets.push_back(transaction_->db());
+  }
+
+  // If this event originated from script, it should have no side effects.
+  if (!event.isTrusted())
+    return IDBEventDispatcher::Dispatch(event, targets);
+  DCHECK(event.type() == event_type_names::kSuccess ||
+         event.type() == event_type_names::kError ||
+         event.type() == event_type_names::kBlocked ||
+         event.type() == event_type_names::kUpgradeneeded)
+      << "event type was " << event.type();
+
   if (!GetExecutionContext())
     return DispatchEventResult::kCanceledBeforeDispatch;
   DCHECK_EQ(ready_state_, PENDING);
@@ -634,17 +657,6 @@
   if (event.type() != event_type_names::kBlocked)
     ready_state_ = DONE;
 
-  HeapVector<Member<EventTarget>> targets;
-  targets.push_back(this);
-  if (transaction_ && !prevent_propagation_) {
-    targets.push_back(transaction_);
-    // If there ever are events that are associated with a database but
-    // that do not have a transaction, then this will not work and we need
-    // this object to actually hold a reference to the database (to ensure
-    // it stays alive).
-    targets.push_back(transaction_->db());
-  }
-
   // Cursor properties should not be updated until the success event is being
   // dispatched.
   IDBCursor* cursor_to_notify = nullptr;
@@ -662,13 +674,6 @@
     did_fire_upgrade_needed_event_ = true;
   }
 
-  // FIXME: When we allow custom event dispatching, this will probably need to
-  // change.
-  DCHECK(event.type() == event_type_names::kSuccess ||
-         event.type() == event_type_names::kError ||
-         event.type() == event_type_names::kBlocked ||
-         event.type() == event_type_names::kUpgradeneeded)
-      << "event type was " << event.type();
   const bool set_transaction_active =
       transaction_ &&
       (event.type() == event_type_names::kSuccess ||
@@ -692,7 +697,6 @@
   // has completed.
   metrics_.RecordAndReset();
 
-  event.SetTarget(this);
   DispatchEventResult dispatch_result =
       IDBEventDispatcher::Dispatch(event, targets);
 
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_transaction.cc b/third_party/blink/renderer/modules/indexeddb/idb_transaction.cc
index 28bf2cc..36b43df2 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_transaction.cc
+++ b/third_party/blink/renderer/modules/indexeddb/idb_transaction.cc
@@ -564,6 +564,21 @@
 
 DispatchEventResult IDBTransaction::DispatchEventInternal(Event& event) {
   IDB_TRACE1("IDBTransaction::dispatchEvent", "txn.id", id_);
+
+  event.SetTarget(this);
+
+  // Per spec: "A transaction's get the parent algorithm returns the
+  // transaction’s connection."
+  HeapVector<Member<EventTarget>> targets;
+  targets.push_back(this);
+  targets.push_back(db());
+
+  // If this event originated from script, it should have no side effects.
+  if (!event.isTrusted())
+    return IDBEventDispatcher::Dispatch(event, targets);
+  DCHECK(event.type() == event_type_names::kComplete ||
+         event.type() == event_type_names::kAbort);
+
   if (!GetExecutionContext()) {
     state_ = kFinished;
     return DispatchEventResult::kCanceledBeforeDispatch;
@@ -574,14 +589,6 @@
   DCHECK_EQ(event.target(), this);
   state_ = kFinished;
 
-  HeapVector<Member<EventTarget>> targets;
-  targets.push_back(this);
-  targets.push_back(db());
-
-  // FIXME: When we allow custom event dispatching, this will probably need to
-  // change.
-  DCHECK(event.type() == event_type_names::kComplete ||
-         event.type() == event_type_names::kAbort);
   DispatchEventResult dispatch_result =
       IDBEventDispatcher::Dispatch(event, targets);
   // FIXME: Try to construct a test where |this| outlives openDBRequest and we
diff --git a/third_party/blink/web_tests/storage/indexeddb/dispatch-events.html b/third_party/blink/web_tests/storage/indexeddb/dispatch-events.html
new file mode 100644
index 0000000..569fe7c
--- /dev/null
+++ b/third_party/blink/web_tests/storage/indexeddb/dispatch-events.html
@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+<title>IndexedDB: Dispatching events from script</title>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<script>
+
+function idb_test(func, name) {
+  async_test(t => {
+    const dbname = location.pathname + ' - ' + t.name;
+    const open_request = indexedDB.open(dbname);
+    t.add_cleanup(() => { indexedDB.deleteDatabase(dbname); });
+    func(t, open_request);
+  }, name);
+}
+
+//
+// IDBOpenDBRequest
+//
+
+// A regression test for http://crbug.com/1032890
+idb_test((t, open_request) => {
+  open_request.onerror = t.unreached_func();
+  open_request.onsuccess = t.step_func_done();
+  open_request.dispatchEvent(new ErrorEvent({}));
+
+}, 'Dispatching a generic event at an IDBOpenDBRequest should not crash');
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onblocked = t.step_func(e => {
+    ++events_seen;
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(events_seen, 1, 'Should have seen event');
+  });
+  open_request.dispatchEvent(new Event('blocked'));
+}, 'Dispatching a synthetic blocked event at an IDBOpenDBRequest');
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.step_func(e => {
+    ++events_seen;
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(events_seen, 1, 'Should have seen event');
+  });
+  open_request.dispatchEvent(new Event('error'));
+}, 'Dispatching a synthetic error event at an IDBOpenDBRequest');
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    ++events_seen;
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(events_seen, 2, 'Should have seen both events');
+  });
+  open_request.dispatchEvent(new Event('upgradeneeded'));
+}, 'Dispatching a synthetic upgradeneeded event at an IDBOpenDBRequest');
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onsuccess = t.step_func(e => {
+    ++events_seen;
+    if (e.isTrusted) {
+      assert_equals(events_seen, 2, 'Should have seen both events');
+      t.done();
+    }
+  });
+  open_request.dispatchEvent(new Event('success'));
+}, 'Dispatching a synthetic success event at an IDBOpenDBRequest');
+
+//
+// IDBTransaction
+//
+
+idb_test((t, open_request) => {
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const tx = open_request.transaction;
+    tx.dispatchEvent(new Event('generic'));
+  });
+  open_request.onsuccess = t.step_func_done();
+}, 'Dispatching a generic event at an IDBTransaction should not crash');
+
+[
+  // Events that would be propagated from an IDBRequest:
+  'success', 'error',
+
+  // Events that would be fired at an IDBTransaction:
+  'abort'
+].forEach(type => {
+  idb_test((t, open_request) => {
+    let events_seen = 0;
+    open_request.onerror = t.unreached_func();
+    open_request.onupgradeneeded = t.step_func(e => {
+      const tx = open_request.transaction;
+      tx.addEventListener(type, t.step_func(e => {
+        assert_false(e.isTrusted, 'Event should not be trusted');
+        ++events_seen;
+      }));
+      tx.dispatchEvent(new Event(type));
+    });
+    open_request.onsuccess = t.step_func_done(e => {
+      assert_equals(events_seen, 1, 'Should have seen event');
+    });
+  }, `Dispatching a synthetic ${type} event at an IDBTransaction`);
+});
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const tx = open_request.transaction;
+    tx.oncomplete = t.step_func(e => {
+      ++events_seen;
+    });
+    tx.dispatchEvent(new Event('complete'));
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(events_seen, 2, 'Should have seen both events');
+  });
+}, 'Dispatching a synthetic complete event at an IDBTransaction');
+
+idb_test((t, open_request) => {
+  const targets = []
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const db = open_request.result;
+    const tx = open_request.transaction;
+    [db, tx].forEach(target => target.addEventListener(
+      'generic',
+      t.step_func(e => { targets.push(e.currentTarget.constructor.name); })
+    ));
+    tx.dispatchEvent(new Event('generic', {bubbles: true}));
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(targets.length, 2, 'Event should propagate');
+    assert_equals(targets[0], 'IDBTransaction',
+                  'First target should be transaction');
+    assert_equals(targets[1], 'IDBDatabase',
+                  'Second target should be database');
+  });
+}, 'Dispatching a generic event at an IDBTransaction propagates correctly');
+
+
+//
+// IDBRequest
+//
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const db = open_request.result;
+    const store = db.createObjectStore('store');
+    const request = store.get(0);
+    request.dispatchEvent(new Event('generic'));
+  });
+  open_request.onsuccess = t.step_func_done();
+}, 'Dispatching a generic event at an IDBRequest should not crash');
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const db = open_request.result;
+    const store = db.createObjectStore('store');
+    const request = store.get(0);
+    request.onerror = t.step_func(e => {
+      ++events_seen;
+    });
+    request.dispatchEvent(new Event('error'));
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(events_seen, 1, 'Should have seen the event');
+  });
+}, 'Dispatching a synthetic error event at an IDBRequest');
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const db = open_request.result;
+    const store = db.createObjectStore('store');
+    const request = store.get(0);
+    request.onsuccess = t.step_func(e => {
+      ++events_seen;
+    });
+    request.dispatchEvent(new Event('success'));
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(events_seen, 2, 'Should have seen both events');
+  });
+}, 'Dispatching a synthetic success event at an IDBRequest');
+
+idb_test((t, open_request) => {
+  const targets = []
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const db = open_request.result;
+    const tx = open_request.transaction;
+    const store = db.createObjectStore('store');
+    const request = store.get(0);
+    [db, tx, request].forEach(target => target.addEventListener(
+      'generic',
+      t.step_func(e => { targets.push(e.currentTarget.constructor.name); })
+    ));
+    request.dispatchEvent(new Event('generic', {bubbles: true}));
+  });
+  open_request.onsuccess = t.step_func_done(e => {
+    assert_equals(targets.length, 3, 'Event should propagate');
+    assert_equals(targets[0], 'IDBRequest',
+                  'First target should be request');
+    assert_equals(targets[1], 'IDBTransaction',
+                  'Second target should be transaction');
+    assert_equals(targets[2], 'IDBDatabase',
+                  'Third target should be database');
+  });
+}, 'Dispatching a generic event at an IDBRequest propagates correctly');
+
+//
+// IDBDatabase
+//
+
+idb_test((t, open_request) => {
+  let events_seen = 0;
+  open_request.onerror = t.unreached_func();
+  open_request.onupgradeneeded = t.step_func(e => {
+    const db = open_request.result;
+    db.dispatchEvent(new Event('generic'));
+  });
+  open_request.onsuccess = t.step_func_done();
+}, 'Dispatching a generic event at an IDBDatabase');
+
+[
+  // Events that would be propagated from an IDBRequest:
+  'success', 'error',
+
+  // Events that would be propagated from an IDBTransaction:
+  'complete', 'abort',
+
+  // Events that would be fired at an IDBDatabase:
+  'versionchange', 'close'
+].forEach(type => {
+   idb_test((t, open_request) => {
+     let events_seen = 0;
+     open_request.onerror = t.unreached_func();
+     open_request.onupgradeneeded = t.step_func(e => {
+       const db = open_request.result;
+       db.addEventListener(type, t.step_func(e => {
+         assert_false(e.isTrusted, 'Event should not be trusted');
+         ++events_seen;
+       }));
+       db.dispatchEvent(new Event(type));
+     });
+     open_request.onsuccess = t.step_func_done(e => {
+       assert_equals(events_seen, 1, 'Should have seen the event');
+     });
+   }, `Dispatching a synthetic ${type} event at an IDBDatabase`);
+ });
+
+</script>