diff --git a/CRM/Batch/BAO/Batch.php b/CRM/Batch/BAO/Batch.php
index 2e4775cb422df461211372e7ea218b2e71c0733c..21d61188a1826089604da2bfa96a147079798d26 100644
--- a/CRM/Batch/BAO/Batch.php
+++ b/CRM/Batch/BAO/Batch.php
@@ -333,7 +333,8 @@ class CRM_Batch_BAO_Batch extends CRM_Batch_DAO_Batch {
         $activityParams = array('source_record_id' => $values['id'], 'activity_type_id' => $aid);
         $exportActivity = CRM_Activity_BAO_Activity::retrieve($activityParams, $val);
         $fid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_EntityFile', $exportActivity->id, 'file_id', 'entity_id');
-        $tokens = array_merge(array('eid' => $exportActivity->id, 'fid' => $fid), $tokens);
+        $fileHash = CRM_Core_BAO_File::generateFileHash($exportActivity->id, $fid);
+        $tokens = array_merge(array('eid' => $exportActivity->id, 'fid' => $fid, 'fcs' => $fileHash), $tokens);
       }
       $values['action'] = CRM_Core_Action::formLink(
         $newLinks,
@@ -486,7 +487,7 @@ class CRM_Batch_BAO_Batch extends CRM_Batch_DAO_Batch {
         'download' => array(
           'name' => ts('Download'),
           'url' => 'civicrm/file',
-          'qs' => 'reset=1&id=%%fid%%&eid=%%eid%%',
+          'qs' => 'reset=1&id=%%fid%%&eid=%%eid%%&fcs=%%fcs%%',
           'title' => ts('Download Batch'),
         ),
       );
diff --git a/CRM/Campaign/Selector/Search.php b/CRM/Campaign/Selector/Search.php
index d7ed429ba5184c57ac38646687ebf7e2870b3431..8d71c4cd1e1e5f9ecf4722219a4a4e6c99a92ee0 100644
--- a/CRM/Campaign/Selector/Search.php
+++ b/CRM/Campaign/Selector/Search.php
@@ -282,12 +282,12 @@ class CRM_Campaign_Selector_Search extends CRM_Core_Selector_Base implements CRM
       );
       list($select, $from) = explode(' FROM ', $sql);
       $selectSQL = "
-      SELECT '$cacheKey', contact_a.id, contact_a.display_name
+      SELECT %1, contact_a.id, contact_a.display_name
 FROM {$from}
 ";
 
       try {
-        Civi::service('prevnext')->fillWithSql($cacheKey, $selectSQL);
+        Civi::service('prevnext')->fillWithSql($cacheKey, $selectSQL, [1 => [$cacheKey, 'String']]);
       }
       catch (CRM_Core_Exception $e) {
         // Heavy handed, no? Seems like this merits an explanation.
diff --git a/CRM/Contact/BAO/Contact/Utils.php b/CRM/Contact/BAO/Contact/Utils.php
index 7a81cb029eb43c2228ae43e013be75ab61c3d663..db4114aa0d9630b928a5cb73d1592c6056a96879 100644
--- a/CRM/Contact/BAO/Contact/Utils.php
+++ b/CRM/Contact/BAO/Contact/Utils.php
@@ -229,7 +229,7 @@ WHERE  id IN ( $idString )
 
     $check = self::generateChecksum($contactID, $inputTS, $inputLF);
 
-    if ($check != $inputCheck) {
+    if (!hash_equals($check, $inputCheck)) {
       return FALSE;
     }
 
diff --git a/CRM/Contact/BAO/Query.php b/CRM/Contact/BAO/Query.php
index 92c47c34bff0c7352c8eeac0ddb31501b660b11a..a935ca96d2c6e1937f4416580822d6eff073520f 100644
--- a/CRM/Contact/BAO/Query.php
+++ b/CRM/Contact/BAO/Query.php
@@ -2972,7 +2972,7 @@ class CRM_Contact_BAO_Query {
         $smartGroupIDs[] = $id;
       }
       else {
-        $regularGroupIDs[] = $id;
+        $regularGroupIDs[] = trim($id);
       }
     }
 
@@ -3011,7 +3011,10 @@ class CRM_Contact_BAO_Query {
       if (count($regularGroupIDs) > 1) {
         $op = strpos($op, 'IN') ? $op : ($op == '!=') ? 'NOT IN' : 'IN';
       }
-      $groupIds = implode(',', (array) $regularGroupIDs);
+      $groupIds = CRM_Utils_Type::validate(
+        implode(',', (array) $regularGroupIDs),
+        'CommaSeparatedIntegers'
+      );
       $gcTable = "`civicrm_group_contact-" . uniqid() . "`";
       $joinClause = array("contact_a.id = {$gcTable}.contact_id");
 
@@ -3173,12 +3176,13 @@ WHERE  $smartGroupClause
 
     $op = "LIKE";
     $value = "%{$value}%";
+    $escapedValue = CRM_Utils_Type::escape("%{$value}%", 'String');
 
     $useAllTagTypes = $this->getWhereValues('all_tag_types', $grouping);
     $tagTypesText = $this->getWhereValues('tag_types_text', $grouping);
 
-    $etTable = "`civicrm_entity_tag-" . $value . "`";
-    $tTable = "`civicrm_tag-" . $value . "`";
+    $etTable = "`civicrm_entity_tag-" . uniqid() . "`";
+    $tTable = "`civicrm_tag-" . uniqid() . "`";
 
     if ($useAllTagTypes[2]) {
       $this->_tables[$etTable] = $this->_whereTables[$etTable]
@@ -3186,8 +3190,8 @@ WHERE  $smartGroupClause
             LEFT JOIN civicrm_tag {$tTable} ON ( {$etTable}.tag_id = {$tTable}.id  )";
 
       // search tag in cases
-      $etCaseTable = "`civicrm_entity_case_tag-" . $value . "`";
-      $tCaseTable = "`civicrm_case_tag-" . $value . "`";
+      $etCaseTable = "`civicrm_entity_case_tag-" . uniqid() . "`";
+      $tCaseTable = "`civicrm_case_tag-" . uniqid() . "`";
       $this->_tables[$etCaseTable] = $this->_whereTables[$etCaseTable]
         = " LEFT JOIN civicrm_case_contact ON civicrm_case_contact.contact_id = contact_a.id
             LEFT JOIN civicrm_case
@@ -3196,8 +3200,8 @@ WHERE  $smartGroupClause
             LEFT JOIN civicrm_entity_tag {$etCaseTable} ON ( {$etCaseTable}.entity_table = 'civicrm_case' AND {$etCaseTable}.entity_id = civicrm_case.id )
             LEFT JOIN civicrm_tag {$tCaseTable} ON ( {$etCaseTable}.tag_id = {$tCaseTable}.id  )";
       // search tag in activities
-      $etActTable = "`civicrm_entity_act_tag-" . $value . "`";
-      $tActTable = "`civicrm_act_tag-" . $value . "`";
+      $etActTable = "`civicrm_entity_act_tag-" . uniqid() . "`";
+      $tActTable = "`civicrm_act_tag-" . uniqid() . "`";
       $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
       $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts);
 
@@ -3210,12 +3214,12 @@ WHERE  $smartGroupClause
             LEFT JOIN civicrm_entity_tag as {$etActTable} ON ( {$etActTable}.entity_table = 'civicrm_activity' AND {$etActTable}.entity_id = civicrm_activity.id )
             LEFT JOIN civicrm_tag {$tActTable} ON ( {$etActTable}.tag_id = {$tActTable}.id  )";
 
-      $this->_where[$grouping][] = "({$tTable}.name $op '" . $value . "' OR {$tCaseTable}.name $op '" . $value . "' OR {$tActTable}.name $op '" . $value . "')";
+      $this->_where[$grouping][] = "({$tTable}.name $op '" . $escapedValue . "' OR {$tCaseTable}.name $op '" . $escapedValue . "' OR {$tActTable}.name $op '" . $escapedValue . "')";
       $this->_qill[$grouping][] = ts('Tag %1 %2', array(1 => $tagTypesText[2], 2 => $op)) . ' ' . $value;
     }
     else {
-      $etTable = "`civicrm_entity_tag-" . $value . "`";
-      $tTable = "`civicrm_tag-" . $value . "`";
+      $etTable = "`civicrm_entity_tag-" . uniqid() . "`";
+      $tTable = "`civicrm_tag-" . uniqid() . "`";
       $this->_tables[$etTable] = $this->_whereTables[$etTable] = " LEFT JOIN civicrm_entity_tag {$etTable} ON ( {$etTable}.entity_id = contact_a.id  AND
       {$etTable}.entity_table = 'civicrm_contact' )
                 LEFT JOIN civicrm_tag {$tTable} ON ( {$etTable}.tag_id = {$tTable}.id  ) ";
@@ -3243,20 +3247,25 @@ WHERE  $smartGroupClause
       if (count($value) > 1) {
         $this->_useDistinct = TRUE;
       }
-      $value = implode(',', (array) $value);
     }
 
+    // implode array, then remove all spaces and validate CommaSeparatedIntegers
+    $value = CRM_Utils_Type::validate(
+      str_replace(' ', '', implode(',', (array) $value)),
+      'CommaSeparatedIntegers'
+    );
+
     $useAllTagTypes = $this->getWhereValues('all_tag_types', $grouping);
     $tagTypesText = $this->getWhereValues('tag_types_text', $grouping);
 
-    $etTable = "`civicrm_entity_tag-" . $value . "`";
+    $etTable = "`civicrm_entity_tag-" . uniqid() . "`";
 
     if ($useAllTagTypes[2]) {
       $this->_tables[$etTable] = $this->_whereTables[$etTable]
         = " LEFT JOIN civicrm_entity_tag {$etTable} ON ( {$etTable}.entity_id = contact_a.id  AND {$etTable}.entity_table = 'civicrm_contact') ";
 
       // search tag in cases
-      $etCaseTable = "`civicrm_entity_case_tag-" . $value . "`";
+      $etCaseTable = "`civicrm_entity_case_tag-" . uniqid() . "`";
       $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
       $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts);
 
@@ -3267,7 +3276,7 @@ WHERE  $smartGroupClause
                 AND civicrm_case.is_deleted = 0 )
             LEFT JOIN civicrm_entity_tag {$etCaseTable} ON ( {$etCaseTable}.entity_table = 'civicrm_case' AND {$etCaseTable}.entity_id = civicrm_case.id ) ";
       // search tag in activities
-      $etActTable = "`civicrm_entity_act_tag-" . $value . "`";
+      $etActTable = "`civicrm_entity_act_tag-" . uniqid() . "`";
       $this->_tables[$etActTable] = $this->_whereTables[$etActTable]
         = " LEFT JOIN civicrm_activity_contact
             ON ( civicrm_activity_contact.contact_id = contact_a.id AND civicrm_activity_contact.record_type_id = {$targetID} )
diff --git a/CRM/Contact/Selector.php b/CRM/Contact/Selector.php
index bd85b81808bc8750a911cf2cb5ddfb58abc929f1..b11e19e6e37bdd1378c176e461cc829dd64fc053 100644
--- a/CRM/Contact/Selector.php
+++ b/CRM/Contact/Selector.php
@@ -1041,11 +1041,11 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     // the other alternative of running the FULL query will just be incredibly inefficient
     // and slow things down way too much on large data sets / complex queries
 
-    $selectSQL = "SELECT DISTINCT '$cacheKey', contact_a.id, contact_a.sort_name";
+    $selectSQL = "SELECT DISTINCT %1, contact_a.id, contact_a.sort_name";
 
     $sql = str_ireplace(array("SELECT contact_a.id as contact_id", "SELECT contact_a.id as id"), $selectSQL, $sql);
     try {
-      Civi::service('prevnext')->fillWithSql($cacheKey, $sql);
+      Civi::service('prevnext')->fillWithSql($cacheKey, $sql, [1 => [$cacheKey, 'String']]);
     }
     catch (CRM_Core_Exception $e) {
       if ($coreSearch) {
diff --git a/CRM/Core/BAO/CustomField.php b/CRM/Core/BAO/CustomField.php
index 8e08547b326f7eda789fed26ce63adc2d9c1185d..f110870e632be12749c08f176ba6654f8ed82d46 100644
--- a/CRM/Core/BAO/CustomField.php
+++ b/CRM/Core/BAO/CustomField.php
@@ -1494,9 +1494,10 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
             'entity_id',
             'file_id'
           );
-          list($path) = CRM_Core_BAO_File::path($fileID, $entityId, NULL, NULL);
+          list($path) = CRM_Core_BAO_File::path($fileID, $entityId);
+          $fileHash = CRM_Core_BAO_File::generateFileHash($entityId, $fileID);
           $url = CRM_Utils_System::url('civicrm/file',
-            "reset=1&id=$fileID&eid=$contactID",
+            "reset=1&id=$fileID&eid=$entityId&fcs=$fileHash",
             $absolute, NULL, TRUE, TRUE
           );
           $result['file_url'] = CRM_Utils_File::getFileURL($path, $fileType, $url);
@@ -1507,8 +1508,9 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
             $fileID,
             'uri'
           );
+          $fileHash = CRM_Core_BAO_File::generateFileHash($contactID, $fileID);
           $url = CRM_Utils_System::url('civicrm/file',
-            "reset=1&id=$fileID&eid=$contactID",
+            "reset=1&id=$fileID&eid=$contactID&fcs=$fileHash",
             $absolute, NULL, TRUE, TRUE
           );
           $result['file_url'] = CRM_Utils_File::getFileURL($uri, $fileType, $url);
diff --git a/CRM/Core/BAO/CustomGroup.php b/CRM/Core/BAO/CustomGroup.php
index 84c46f02c68faef0b33cddf0c576b26b900aa726..df5d09adaf051f8bf34fc06cb281a4a4bc3a59f3 100644
--- a/CRM/Core/BAO/CustomGroup.php
+++ b/CRM/Core/BAO/CustomGroup.php
@@ -875,17 +875,18 @@ ORDER BY civicrm_custom_group.weight,
 
         if ($fileDAO->find(TRUE)) {
           $entityIDName = "{$table}_entity_id";
+          $fileHash = CRM_Core_BAO_File::generateFileHash($dao->$entityIDName, $fileDAO->id);
           $customValue['id'] = $dao->$idName;
           $customValue['data'] = $fileDAO->uri;
           $customValue['fid'] = $fileDAO->id;
-          $customValue['fileURL'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$fileDAO->id}&eid={$dao->$entityIDName}");
+          $customValue['fileURL'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$fileDAO->id}&eid={$dao->$entityIDName}&fcs=$fileHash");
           $customValue['displayURL'] = NULL;
           $deleteExtra = ts('Are you sure you want to delete attached file.');
           $deleteURL = array(
             CRM_Core_Action::DELETE => array(
               'name' => ts('Delete Attached File'),
               'url' => 'civicrm/file',
-              'qs' => 'reset=1&id=%%id%%&eid=%%eid%%&fid=%%fid%%&action=delete',
+              'qs' => 'reset=1&id=%%id%%&eid=%%eid%%&fid=%%fid%%&action=delete&fcs=%%fcs%%',
               'extra' => 'onclick = "if (confirm( \'' . $deleteExtra
               . '\' ) ) this.href+=\'&confirmed=1\'; else return false;"',
             ),
@@ -896,6 +897,7 @@ ORDER BY civicrm_custom_group.weight,
               'id' => $fileDAO->id,
               'eid' => $dao->$entityIDName,
               'fid' => $fieldID,
+              'fcs' => $fileHash,
             ),
             ts('more'),
             FALSE,
@@ -919,7 +921,7 @@ ORDER BY civicrm_custom_group.weight,
             );
             $customValue['imageURL'] = str_replace('persist/contribute', 'custom', $config->imageUploadURL) .
               $fileDAO->uri;
-            list($path) = CRM_Core_BAO_File::path($fileDAO->id, $entityId, NULL, NULL);
+            list($path) = CRM_Core_BAO_File::path($fileDAO->id, $entityId);
             if ($path && file_exists($path)) {
               list($imageWidth, $imageHeight) = getimagesize($path);
               list($imageThumbWidth, $imageThumbHeight) = CRM_Contact_BAO_Contact::getThumbSize($imageWidth, $imageHeight);
diff --git a/CRM/Core/BAO/File.php b/CRM/Core/BAO/File.php
index ccbc7532729783f2c36f8d73e1bdd5b1c19b75f7..5e1a32def07ba55d2a1b3ae07758d6dbc1f6eb71 100644
--- a/CRM/Core/BAO/File.php
+++ b/CRM/Core/BAO/File.php
@@ -71,15 +71,11 @@ class CRM_Core_BAO_File extends CRM_Core_DAO_File {
   /**
    * @param int $fileID
    * @param int $entityID
-   * @param null $entityTable
    *
    * @return array
    */
-  public static function path($fileID, $entityID, $entityTable = NULL) {
+  public static function path($fileID, $entityID) {
     $entityFileDAO = new CRM_Core_DAO_EntityFile();
-    if ($entityTable) {
-      $entityFileDAO->entity_table = $entityTable;
-    }
     $entityFileDAO->entity_id = $entityID;
     $entityFileDAO->file_id = $fileID;
 
@@ -337,6 +333,7 @@ class CRM_Core_BAO_File extends CRM_Core_DAO_File {
     $dao = CRM_Core_DAO::executeQuery($sql, $params);
     $results = array();
     while ($dao->fetch()) {
+      $fileHash = self::generateFileHash($dao->entity_id, $dao->cfID);
       $result['fileID'] = $dao->cfID;
       $result['entityID'] = $dao->cefID;
       $result['mime_type'] = $dao->mime_type;
@@ -344,7 +341,7 @@ class CRM_Core_BAO_File extends CRM_Core_DAO_File {
       $result['description'] = $dao->description;
       $result['cleanName'] = CRM_Utils_File::cleanFileName($dao->uri);
       $result['fullPath'] = $config->customFileUploadDir . DIRECTORY_SEPARATOR . $dao->uri;
-      $result['url'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$dao->cfID}&eid={$dao->entity_id}");
+      $result['url'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$dao->cfID}&eid={$dao->entity_id}&fcs={$fileHash}");
       $result['href'] = "<a href=\"{$result['url']}\">{$result['cleanName']}</a>";
       $result['tag'] = CRM_Core_BAO_EntityTag::getTag($dao->cfID, 'civicrm_file');
       $result['icon'] = CRM_Utils_File::getIconFromMimeType($dao->mime_type);
@@ -770,4 +767,56 @@ AND       CEF.entity_id    = %2";
     return NULL;
   }
 
+  /**
+   * Generates an access-token for downloading a specific file.
+   *
+   * @param int $entityId entity id the file is attached to
+   * @param int $fileId file ID
+   * @return string
+   */
+  public static function generateFileHash($entityId = NULL, $fileId = NULL, $genTs = NULL, $life = NULL) {
+    // Use multiple (but stable) inputs for hash information.
+    $siteKey = CRM_Utils_Constant::value('CIVICRM_SITE_KEY');
+    if (!$siteKey) {
+      throw new \CRM_Core_Exception("Cannot generate file access token. Please set CIVICRM_SITE_KEY.");
+    }
+
+    if (!$genTs) {
+      $genTs = time();
+    }
+    if (!$life) {
+      $days = Civi::settings()->get('checksum_timeout');
+      $life = 24 * $days;
+    }
+    // Trim 8 chars off the string, make it slightly easier to find
+    // but reveals less information from the hash.
+    $cs = hash_hmac('sha256', "entity={$entityId}&file={$fileId}&life={$life}", $siteKey);
+    return "{$cs}_{$genTs}_{$life}";
+  }
+
+  /**
+   * Validate a file access token.
+   *
+   * @param string $hash
+   * @param int $entityId Entity Id the file is attached to
+   * @param int $fileId File Id
+   * @return bool
+   */
+  public static function validateFileHash($hash, $entityId, $fileId) {
+    $input = CRM_Utils_System::explode('_', $hash, 3);
+    $inputTs = CRM_Utils_Array::value(1, $input);
+    $inputLF = CRM_Utils_Array::value(2, $input);
+    $testHash = CRM_Core_BAO_File::generateFileHash($entityId, $fileId, $inputTs, $inputLF);
+    if (hash_equals($testHash, $hash)) {
+      $now = time();
+      if ($inputTs + ($inputLF * 60 * 60) >= $now) {
+        return TRUE;
+      }
+      else {
+        return FALSE;
+      }
+    }
+    return FALSE;
+  }
+
 }
diff --git a/CRM/Core/Form/Renderer.php b/CRM/Core/Form/Renderer.php
index 230cfa142fd4167d376762db1dbe9946304e70fa..82847220267918fede00c27adff8e6d09021bde6 100644
--- a/CRM/Core/Form/Renderer.php
+++ b/CRM/Core/Form/Renderer.php
@@ -248,6 +248,14 @@ class CRM_Core_Form_Renderer extends HTML_QuickForm_Renderer_ArraySmarty {
       $params = $field->getAttribute('data-api-params');
       $params = $params ? json_decode($params, TRUE) : array();
       $result = civicrm_api3($entity, 'getlist', array('id' => $val) + $params);
+      // Purify label output of entityreference fields
+      if (!empty($result['values'])) {
+        foreach ($result['values'] as &$res) {
+          if (!empty($res['label'])) {
+            $res['label'] = CRM_Utils_String::purifyHTML($res['label']);
+          }
+        }
+      }
       if ($field->isFrozen()) {
         // Prevent js from treating frozen entityRef as a "live" field
         $field->removeAttribute('class');
@@ -299,7 +307,7 @@ class CRM_Core_Form_Renderer extends HTML_QuickForm_Renderer_ArraySmarty {
       foreach (explode(',', $val) as $item) {
         $match = CRM_Utils_Array::findInTree($item, $params['data']);
         if (isset($match['text']) && strlen($match['text'])) {
-          $display[] = $match['text'];
+          $display[] = CRM_Utils_String::purifyHTML($match['text']);
         }
       }
       $el['html'] = implode('; ', $display) . '<input type="hidden" value="' . $field->getValue() . '" name="' . $field->getAttribute('name') . '">';
@@ -327,7 +335,7 @@ class CRM_Core_Form_Renderer extends HTML_QuickForm_Renderer_ArraySmarty {
       // Format contact as link
       if ($entity == 'contact' && CRM_Contact_BAO_Contact_Permission::allow($val['id'], CRM_Core_Permission::VIEW)) {
         $url = CRM_Utils_System::url("civicrm/contact/view", array('reset' => 1, 'cid' => $val['id']));
-        $val['label'] = '<a class="view-' . $entity . ' no-popup" href="' . $url . '" title="' . ts('View Contact') . '">' . $val['label'] . '</a>';
+        $val['label'] = '<a class="view-' . $entity . ' no-popup" href="' . $url . '" title="' . ts('View Contact') . '">' . CRM_Utils_String::purifyHTML($val['label']) . '</a>';
       }
       $display[] = $val['label'];
     }
diff --git a/CRM/Core/Page/File.php b/CRM/Core/Page/File.php
index e2266039e77f896fa77fe0214d70c0159d90c3c5..76d65e0a2f4c28dcad1fa1ce499dea9f683df9f5 100644
--- a/CRM/Core/Page/File.php
+++ b/CRM/Core/Page/File.php
@@ -38,23 +38,21 @@ class CRM_Core_Page_File extends CRM_Core_Page {
    * Run page.
    */
   public function run() {
-    $fileName = CRM_Utils_Request::retrieve('filename', 'String', $this);
-    $path = CRM_Core_Config::singleton()->customFileUploadDir . $fileName;
-    $mimeType = CRM_Utils_Request::retrieve('mime-type', 'String', $this);
     $action = CRM_Utils_Request::retrieve('action', 'String', $this);
     $download = CRM_Utils_Request::retrieve('download', 'Integer', $this, FALSE, 1);
     $disposition = $download == 0 ? 'inline' : 'download';
 
-    // if we are not providing essential parameter needed for file preview then
-    if (empty($fileName) && empty($mimeType)) {
-      $eid = CRM_Utils_Request::retrieve('eid', 'Positive', $this, TRUE);
-      $fid = CRM_Utils_Request::retrieve('fid', 'Positive', $this, FALSE);
-      $id = CRM_Utils_Request::retrieve('id', 'Positive', $this, TRUE);
-      $quest = CRM_Utils_Request::retrieve('quest', 'String', $this);
-
-      list($path, $mimeType) = CRM_Core_BAO_File::path($id, $eid, NULL, $quest);
+    $entityId = CRM_Utils_Request::retrieve('eid', 'Positive', $this, TRUE); // Entity ID (e.g. Contact ID)
+    $fieldId = CRM_Utils_Request::retrieve('fid', 'Positive', $this, FALSE); // Field ID
+    $fileId = CRM_Utils_Request::retrieve('id', 'Positive', $this, TRUE); // File ID
+    $hash = CRM_Utils_Request::retrieve('fcs', 'Alphanumeric', $this);
+    if (!CRM_Core_BAO_File::validateFileHash($hash, $entityId, $fileId)) {
+      CRM_Core_Error::statusBounce('URL for file is not valid');
     }
 
+    list($path, $mimeType) = CRM_Core_BAO_File::path($fileId, $entityId);
+    $mimeType = CRM_Utils_Request::retrieveValue('mime-type', 'String', $mimeType, FALSE);
+
     if (!$path) {
       CRM_Core_Error::statusBounce('Could not retrieve the file');
     }
@@ -66,7 +64,7 @@ class CRM_Core_Page_File extends CRM_Core_Page {
 
     if ($action & CRM_Core_Action::DELETE) {
       if (CRM_Utils_Request::retrieve('confirmed', 'Boolean')) {
-        CRM_Core_BAO_File::deleteFileReferences($id, $eid, $fid);
+        CRM_Core_BAO_File::deleteFileReferences($fileId, $entityId, $fieldId);
         CRM_Core_Session::setStatus(ts('The attached file has been deleted.'), ts('Complete'), 'success');
 
         $session = CRM_Core_Session::singleton();
diff --git a/CRM/Core/PrevNextCache/Interface.php b/CRM/Core/PrevNextCache/Interface.php
index 33ce6dab7538595a1048b6168b6ba451a93b1c22..6c355050e7577fdb4c6c2e1cd67a4b4464eb5efa 100644
--- a/CRM/Core/PrevNextCache/Interface.php
+++ b/CRM/Core/PrevNextCache/Interface.php
@@ -40,9 +40,14 @@ interface CRM_Core_PrevNextCache_Interface {
    * @param string $sql
    *   A SQL query. The query *MUST* be a SELECT statement which yields
    *   the following columns (in order): cacheKey, entity_id1, data
+   * @param array $sqlParams
+   *   An array of parameters to be used with $sql.
+   *   Use the same interpolation format as CRM_Core_DAO (composeQuery/executeQuery).
+   *   Ex: [1 => ['foo', 'String']]
    * @return bool
+   * @see CRM_Core_DAO::composeQuery
    */
-  public function fillWithSql($cacheKey, $sql);
+  public function fillWithSql($cacheKey, $sql, $sqlParams = []);
 
   /**
    * Store the contents of an array in the cache.
diff --git a/CRM/Core/PrevNextCache/Redis.php b/CRM/Core/PrevNextCache/Redis.php
index ff4a0f3d6d686afce38a457036e48999946d1e43..99986f4d714423fdf6ebaa545833479697953bab 100644
--- a/CRM/Core/PrevNextCache/Redis.php
+++ b/CRM/Core/PrevNextCache/Redis.php
@@ -61,8 +61,8 @@ class CRM_Core_PrevNextCache_Redis implements CRM_Core_PrevNextCache_Interface {
     $this->prefix .= \CRM_Utils_Cache::DELIMITER . 'prevnext' . \CRM_Utils_Cache::DELIMITER;
   }
 
-  public function fillWithSql($cacheKey, $sql) {
-    $dao = CRM_Core_DAO::executeQuery($sql, [], FALSE, NULL, FALSE, TRUE, TRUE);
+  public function fillWithSql($cacheKey, $sql, $sqlParams = []) {
+    $dao = CRM_Core_DAO::executeQuery($sql, $sqlParams, FALSE, NULL, FALSE, TRUE, TRUE);
     if (is_a($dao, 'DB_Error')) {
       throw new CRM_Core_Exception($dao->message);
     }
diff --git a/CRM/Core/PrevNextCache/Sql.php b/CRM/Core/PrevNextCache/Sql.php
index 953dee024303dfb4462795b2c6ea9d1dd842f225..efa1756a0219b7e932d01a79b453b676bca95ab4 100644
--- a/CRM/Core/PrevNextCache/Sql.php
+++ b/CRM/Core/PrevNextCache/Sql.php
@@ -38,14 +38,19 @@ class CRM_Core_PrevNextCache_Sql implements CRM_Core_PrevNextCache_Interface {
    * @param string $sql
    *   A SQL query. The query *MUST* be a SELECT statement which yields
    *   the following columns (in order): cacheKey, entity_id1, data
+   * @param array $sqlParams
+   *   An array of parameters to be used with $sql.
+   *   Use the same interpolation format as CRM_Core_DAO (composeQuery/executeQuery).
+   *   Ex: [1 => ['foo', 'String']]
    * @return bool
    * @throws CRM_Core_Exception
+   * @see CRM_Core_DAO::composeQuery
    */
-  public function fillWithSql($cacheKey, $sql) {
+  public function fillWithSql($cacheKey, $sql, $sqlParams = []) {
     $insertSQL = "
 INSERT INTO civicrm_prevnext_cache (cacheKey, entity_id1, data)
 ";
-    $result = CRM_Core_DAO::executeQuery($insertSQL . $sql, [], FALSE, NULL, FALSE, TRUE, TRUE);
+    $result = CRM_Core_DAO::executeQuery($insertSQL . $sql, $sqlParams, FALSE, NULL, FALSE, TRUE, TRUE);
     if (is_a($result, 'DB_Error')) {
       throw new CRM_Core_Exception($result->message);
     }
diff --git a/CRM/PCP/Page/PCPInfo.php b/CRM/PCP/Page/PCPInfo.php
index bd51d17df227f5e3200f9f8428cb7f6b388933f4..3a3c2b34bf91169341d663e5416ea08b4b869cd5 100644
--- a/CRM/PCP/Page/PCPInfo.php
+++ b/CRM/PCP/Page/PCPInfo.php
@@ -202,8 +202,9 @@ class CRM_PCP_Page_PCPInfo extends CRM_Core_Page {
     if (!empty($entityFile)) {
       $fileInfo = reset($entityFile);
       $fileId = $fileInfo['fileID'];
+      $fileHash = CRM_Core_BAO_File::generateFileHash($this->_id, $fileId);
       $image = '<img src="' . CRM_Utils_System::url('civicrm/file',
-          "reset=1&id=$fileId&eid={$this->_id}"
+          "reset=1&id=$fileId&eid={$this->_id}&fcs={$fileHash}"
         ) . '" />';
       $this->assign('image', $image);
     }
diff --git a/CRM/Profile/Form.php b/CRM/Profile/Form.php
index 7dfdbdd10f40906cb709eddbf2ec98e5712140ca..330b50890a0d3232eb4d985aa93acf6925af920d 100644
--- a/CRM/Profile/Form.php
+++ b/CRM/Profile/Form.php
@@ -511,8 +511,9 @@ class CRM_Profile_Form extends CRM_Core_Form {
 
               $deleteExtra = ts("Are you sure you want to delete attached file?");
               $fileId = $url['file_id'];
+              $fileHash = CRM_Core_BAO_File::generateFileHash($entityId, $fileId);
               $deleteURL = CRM_Utils_System::url('civicrm/file',
-                "reset=1&id={$fileId}&eid=$entityId&fid={$key}&action=delete"
+                "reset=1&id={$fileId}&eid=$entityId&fid={$key}&action=delete&fcs={$fileHash}"
               );
               $text = ts("Delete Attached File");
               $customFiles[$field['name']]['deleteURL'] = "<a href=\"{$deleteURL}\" onclick = \"if (confirm( ' $deleteExtra ' )) this.href+='&amp;confirmed=1'; else return false;\">$text</a>";
@@ -551,8 +552,9 @@ class CRM_Profile_Form extends CRM_Core_Form {
 
               $deleteExtra = ts("Are you sure you want to delete attached file?");
               $fileId = $url['file_id'];
+              $fileHash = CRM_Core_BAO_File::generateFileHash($entityId, $fileId); /* fieldId=$customFieldID */
               $deleteURL = CRM_Utils_System::url('civicrm/file',
-                "reset=1&id={$fileId}&eid=$entityId&fid={$customFieldID}&action=delete"
+                "reset=1&id={$fileId}&eid=$entityId&fid={$customFieldID}&action=delete&fcs={$fileHash}"
               );
               $text = ts("Delete Attached File");
               $customFiles[$field['name']]['deleteURL'] = "<a href=\"{$deleteURL}\" onclick = \"if (confirm( ' $deleteExtra ' )) this.href+='&amp;confirmed=1'; else return false;\">$text</a>";
diff --git a/CRM/Utils/Money.php b/CRM/Utils/Money.php
index f8afa6cf5c9cde3b681c7fd8e4b3898a1913c9ad..6f95398353c35d77237f909bb1234016438b9fd4 100644
--- a/CRM/Utils/Money.php
+++ b/CRM/Utils/Money.php
@@ -95,6 +95,13 @@ class CRM_Utils_Money {
     if (!$currency) {
       $currency = $config->defaultCurrency;
     }
+
+    // ensure $currency is a valid currency code
+    // for backwards-compatibility, also accept one space instead of a currency
+    if ($currency != ' ' && !array_key_exists($currency, self::$_currencySymbols)) {
+      throw new CRM_Core_Exception("Invalid currency \"{$currency}\"");
+    }
+
     $amount = self::formatNumericByFormat($amount, $valueFormat);
     // If it contains tags, means that HTML was passed and the
     // amount is already converted properly,
diff --git a/api/v3/Attachment.php b/api/v3/Attachment.php
index e9bc6cad95e313a38d884a18997e3e4eebff2679..a96a35886dbb01f392df9f541924ce6a33b7cfc0 100644
--- a/api/v3/Attachment.php
+++ b/api/v3/Attachment.php
@@ -435,8 +435,9 @@ function _civicrm_api3_attachment_format_result($fileDao, $entityFileDao, $retur
     'icon' => CRM_Utils_File::getIconFromMimeType($fileDao->mime_type),
     'created_id' => $fileDao->created_id,
   );
+  $fileHash = CRM_Core_BAO_File::generateFileHash($result['entity_id'], $result['id']);
   $result['url'] = CRM_Utils_System::url(
-    'civicrm/file', 'reset=1&id=' . $result['id'] . '&eid=' . $result['entity_id'],
+    'civicrm/file', 'reset=1&id=' . $result['id'] . '&eid=' . $result['entity_id'] . '&fcs=' . $fileHash,
     TRUE,
     NULL,
     FALSE,
diff --git a/js/Common.js b/js/Common.js
index de326cda0e367aa6c7e91e8e0b2eb82e5e5f899a..93f7b1032f3574e03a46b012ca8f7a3f91df05af 100644
--- a/js/Common.js
+++ b/js/Common.js
@@ -1544,4 +1544,11 @@ if (!CRM.vars) CRM.vars = {};
     return (yiq >= 128) ? 'black' : 'white';
   };
 
+  // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
+  $.ajaxPrefilter(function(s) {
+    if (s.crossDomain) {
+      s.contents.script = false;
+    }
+  });
+
 })(jQuery, _);
diff --git a/release-notes/5.10.3.md b/release-notes/5.10.3.md
new file mode 100644
index 0000000000000000000000000000000000000000..175c792ff8194984c0b0ef38c59ee2d1de7f65c3
--- /dev/null
+++ b/release-notes/5.10.3.md
@@ -0,0 +1,64 @@
+# CiviCRM 5.10.3
+
+Released February 20, 2019
+
+- **[Synopsis](#synopsis)**
+- **[Security advisories](#security)**
+- **[Bugs resolved](#bugs)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?*                                         |         |
+|:--------------------------------------------------------------- |:-------:|
+| **Fix security vulnerabilities?**                               | **yes** |
+| Change the database schema?                                     |   no    |
+| Alter the API?                                                  |   no    |
+| Require attention to configuration options?                     |   no    |
+| Fix problems installing or upgrading to a previous version?     |   no    |
+| Introduce features?                                             |   no    |
+| **Fix bugs?**                                                   | **yes** |
+
+## <a name="security"></a>Security advisories
+- **[CIVI-SA-2019-01](https://civicrm.org/advisory/civi-sa-2019-01-weak-access-control-for-file-attachments)**:
+  Weak access-control for file attachments
+- **[CIVI-SA-2019-02](https://civicrm.org/advisory/civi-sa-2019-02-sqli-in-prevnext-cache)**:
+  SQL Injection in "PrevNext" Cache
+- **[CIVI-SA-2019-03](https://civicrm.org/advisory/civi-sa-2019-03-xss-in-logging-details-report)**:
+  Cross-Site Scripting in "Logging Details" Report
+- **[CIVI-SA-2019-04](https://civicrm.org/advisory/civi-sa-2019-04-sqli-in-group-tag-filters)**:
+  SQL Injection in Group and Tag Filters
+- **[CIVI-SA-2019-05](https://civicrm.org/advisory/civi-sa-2019-05-xss-in-new-pledge-form)**:
+  Cross-Site Scripting in "New Pledge" Form
+- **[CIVI-SA-2019-06](https://civicrm.org/advisory/civi-sa-2019-06-xss-in-contact-entity-reference-fields)**:
+  Cross-Site Scripting in Contact Reference Fields
+- **[CIVI-SA-2019-07](https://civicrm.org/advisory/civi-sa-2019-07-limit-cross-domain-execution-by-jquery)**:
+  Limit Cross-Domain Execution by jQuery
+
+## <a name="bugs"></a>Bugs resolved
+
+### Core CiviCRM
+
+- **[dev/core#695](https://lab.civicrm.org/dev/core/issues/695) Custom Search
+  results selection failure and
+  [dev/core#679](https://lab.civicrm.org/dev/core/issues/679) Groups and Tags
+  affect search results when using Search Builder
+  ([13533](https://github.com/civicrm/civicrm-core/pull/13533))**
+
+  This resolves some search regressions introduced in 5.9.0 relating to caching
+  and custom searches.
+
+- **[dev/core#737](https://lab.civicrm.org/dev/core/issues/737) Mass SMS not
+  sent when send time is set to immediately 
+  ([13641](https://github.com/civicrm/civicrm-core/pull/13641))**
+
+  This resolves an issue where if you selected to send a Bulk SMS immediately
+  it would not be sent because the scheduled date was set to NULL rather than
+  the current date and time.
+
+## <a name="feedback"></a>Feedback
+
+Security release notes are edited by Seamus Lee and Tim Otten, and release
+notes generally are edited by Andrew Hunt.  If you'd like to provide
+feedback on them, please login to https://chat.civicrm.org/civicrm and
+contact `@agh1`.
diff --git a/templates/CRM/Logging/ReportDetail.tpl b/templates/CRM/Logging/ReportDetail.tpl
index 1cd5ed2a64ac3f2c6f8dbe1ccae5be23e8baac9d..0c23360c848e630c50645695b3db43359fdd68f0 100644
--- a/templates/CRM/Logging/ReportDetail.tpl
+++ b/templates/CRM/Logging/ReportDetail.tpl
@@ -35,7 +35,7 @@
         </dl>
       </div>
     {/if}
-    <p>{ts 1=$whom_url 2=$whom_name 3=$who_url 4=$who_name 5=$log_date}Change to <a href='%1'>%2</a> made by <a href='%3'>%4</a> on %5:{/ts}</p>
+    <p>{ts 1=$whom_url 2=$whom_name|escape 3=$who_url 4=$who_name|escape 5=$log_date}Change to <a href='%1'>%2</a> made by <a href='%3'>%4</a> on %5:{/ts}</p>
     {if $layout eq 'overlay'}
       {include file="CRM/Report/Form/Layout/Overlay.tpl"}
     {else}
diff --git a/tests/phpunit/CRM/Core/Smarty/plugins/CrmMoneyTest.php b/tests/phpunit/CRM/Core/Smarty/plugins/CrmMoneyTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a2f62f7fa62b03914639c29a352e2c07c999ce6
--- /dev/null
+++ b/tests/phpunit/CRM/Core/Smarty/plugins/CrmMoneyTest.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * Class CRM_Core_Smarty_plugins_CrmMoneyTest
+ * @group headless
+ */
+class CRM_Core_Smarty_plugins_CrmMoneyTest extends CiviUnitTestCase {
+  public function setUp() {
+    parent::setUp();
+    require_once 'CRM/Core/Smarty.php';
+
+    // Templates should normally be file names, but for unit-testing it's handy to use "string:" notation
+    require_once 'CRM/Core/Smarty/resources/String.php';
+    civicrm_smarty_register_string_resource();
+  }
+
+  /**
+   * @return array
+   */
+  public function moneyCases() {
+    $cases = [];
+    $cases[] = ['$ 4.00', '{assign var="amount" value="4.00"}{$amount|crmMoney:USD}'];
+    $cases[] = ['€ 1,234.00', '{assign var="amount" value="1234.00"}{$amount|crmMoney:EUR}'];
+    $cases[] = [
+      '$ <input size="10" style="background-color:#EBECE4" readonly="readonly" name="eachPaymentAmount" type="text" id="eachPaymentAmount" class="crm-form-text">',
+      '{assign var="amount" value=\'<input size="10" style="background-color:#EBECE4" readonly="readonly" name="eachPaymentAmount" type="text" id="eachPaymentAmount" class="crm-form-text">\'}{$amount|crmMoney:USD}'
+    ];
+    return $cases;
+  }
+
+  /**
+   * @dataProvider moneyCases
+   * @param $expected
+   * @param $input
+   */
+  public function testMoney($expected, $input) {
+    $smarty = CRM_Core_Smarty::singleton();
+    $actual = $smarty->fetch('string:' . $input);
+    $this->assertEquals($expected, $actual, "Process input=[$input]");
+  }
+
+}
diff --git a/tests/phpunit/CRM/Utils/MoneyTest.php b/tests/phpunit/CRM/Utils/MoneyTest.php
index d8b8b6635afb9a4e6cfb3654ab5e9494c3e99b78..bd343e51710cad7f94c8623921c2b5a1036e241c 100644
--- a/tests/phpunit/CRM/Utils/MoneyTest.php
+++ b/tests/phpunit/CRM/Utils/MoneyTest.php
@@ -71,4 +71,19 @@ class CRM_Utils_MoneyTest extends CiviUnitTestCase {
     $this->setCurrencySeparators(',');
   }
 
+  /**
+   * Test that using the space character as a currency works
+   */
+  public function testSpaceCurrency() {
+    $this->assertEquals('  8,950.37', CRM_Utils_Money::format(8950.37, ' '));
+  }
+
+  /**
+   * Test that passing an invalid currency throws an error
+   */
+  public function testInvalidCurrency() {
+    $this->setExpectedException(CRM_Core_Exception::class, 'Invalid currency "NOT_A_CURRENCY"');
+    CRM_Utils_Money::format(4.00, 'NOT_A_CURRENCY');
+  }
+
 }
diff --git a/tests/phpunit/E2E/Core/PrevNextTest.php b/tests/phpunit/E2E/Core/PrevNextTest.php
index d416e564b8141e49b34e9c21991cb0990b1f2f7d..ed1fdebae63b1241a4d5c7b60026066860880cb2 100644
--- a/tests/phpunit/E2E/Core/PrevNextTest.php
+++ b/tests/phpunit/E2E/Core/PrevNextTest.php
@@ -45,11 +45,11 @@ class PrevNextTest extends \CiviEndToEndTestCase {
     $query = new \CRM_Contact_BAO_Query(array(), NULL, NULL, FALSE, FALSE, 1, FALSE, TRUE, FALSE, NULL, 'AND');
     $sql = $query->searchQuery($start, $prefillLimit, $sort, FALSE, $query->_includeContactIds,
       FALSE, TRUE, TRUE);
-    $selectSQL = "SELECT DISTINCT '$this->cacheKey', contact_a.id, contact_a.sort_name";
+    $selectSQL = "SELECT DISTINCT %1, contact_a.id, contact_a.sort_name";
     $sql = str_replace(array("SELECT contact_a.id as contact_id", "SELECT contact_a.id as id"), $selectSQL, $sql);
 
     $this->assertTrue(
-      $this->prevNext->fillWithSql($this->cacheKey, $sql),
+      $this->prevNext->fillWithSql($this->cacheKey, $sql, [1 => [$this->cacheKey, 'String']]),
       "fillWithSql should return TRUE on success"
     );
 
diff --git a/tests/phpunit/api/v3/AttachmentTest.php b/tests/phpunit/api/v3/AttachmentTest.php
index d98b4ca58504413a0afa4ab3b66369da75c1b5b2..f2721ec852fc3751dd53cf58aacd4f5ddf5904c3 100644
--- a/tests/phpunit/api/v3/AttachmentTest.php
+++ b/tests/phpunit/api/v3/AttachmentTest.php
@@ -505,6 +505,14 @@ class api_v3_AttachmentTest extends CiviUnitTestCase {
 
     $getResult = $this->callAPISuccess('Attachment', 'get', $getParams);
     $actualNames = array_values(CRM_Utils_Array::collect('name', $getResult['values']));
+    // Verify the hash generated by the API is valid if we were to try and load the file.
+    foreach ($getResult['values'] as $result) {
+      $queryResult = [];
+      $parsedURl = parse_url($result['url']);
+      $parsedQuery = parse_str($parsedURl['query'], $queryResult);
+      $this->assertTrue(CRM_Core_BAO_File::validateFileHash($queryResult['fcs'], $queryResult['eid'], $queryResult['id']));
+    }
+
     sort($actualNames);
     sort($expectedNames);
     $this->assertEquals($expectedNames, $actualNames);
diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php
index 3ae1d260201dceb8fefdac53b2ec13f8a052eedf..f2f53c32ec962d79a8d80bb6cca86b4fc894a840 100644
--- a/tests/phpunit/api/v3/ContactTest.php
+++ b/tests/phpunit/api/v3/ContactTest.php
@@ -3929,4 +3929,69 @@ class api_v3_ContactTest extends CiviUnitTestCase {
     $this->assertEquals($note['values'][$note['id']]['note'], "Test note created by API Call as array");
   }
 
+  /**
+   * Verify that passing tag IDs to Contact.get works
+   *
+   * Tests the following formats
+   * - Contact.get tag='id1'
+   * - Contact.get tag='id1,id2'
+   * - Contact.get tag='id1, id2'
+   */
+  public function testContactGetWithTag() {
+    $contact = $this->callApiSuccess('Contact', 'create', [
+      'contact_type' => 'Individual',
+      'first_name' => 'Test',
+      'last_name' => 'Tagged',
+      'email' => 'test@example.org',
+    ]);
+    $tags = [];
+    foreach (['Tag A', 'Tag B'] as $name) {
+      $tags[] = $this->callApiSuccess('Tag', 'create', [
+        'name' => $name
+      ]);
+    }
+
+    // assign contact to "Tag B"
+    $this->callApiSuccess('EntityTag', 'create', [
+      'entity_table' => 'civicrm_contact',
+      'entity_id' => $contact['id'],
+      'tag_id' => $tags[1]['id'],
+    ]);
+
+    // test format Contact.get tag='id1'
+    $contact_get = $this->callAPISuccess('Contact', 'get', [
+      'tag' => $tags[1]['id'],
+      'return' => 'tag',
+    ]);
+    $this->assertEquals(1, $contact_get['count']);
+    $this->assertEquals($contact['id'], $contact_get['id']);
+    $this->assertEquals('Tag B', $contact_get['values'][$contact['id']]['tags']);
+
+    // test format Contact.get tag='id1,id2'
+    $contact_get = $this->callAPISuccess('Contact', 'get', [
+      'tag' => $tags[0]['id'] . ',' . $tags[1]['id'],
+      'return' => 'tag',
+    ]);
+    $this->assertEquals(1, $contact_get['count']);
+    $this->assertEquals($contact['id'], $contact_get['id']);
+    $this->assertEquals('Tag B', $contact_get['values'][$contact['id']]['tags']);
+
+    // test format Contact.get tag='id1, id2'
+    $contact_get = $this->callAPISuccess('Contact', 'get', [
+      'tag' => $tags[0]['id'] . ', ' . $tags[1]['id'],
+      'return' => 'tag',
+    ]);
+    $this->assertEquals(1, $contact_get['count']);
+    $this->assertEquals($contact['id'], $contact_get['id']);
+    $this->assertEquals('Tag B', $contact_get['values'][$contact['id']]['tags']);
+
+    foreach ($tags as $tag) {
+      $this->callAPISuccess('Tag', 'delete', ['id' => $tag['id']]);
+    }
+    $this->callAPISuccess('Contact', 'delete', [
+      'id' => $contact['id'],
+      'skip_undelete' => TRUE
+    ]);
+  }
+
 }