diff --git a/civicrm.php b/civicrm.php
index 8e1297af005a42631fc7038a174bb7266fb2e8ef..e1fa3472fce3de9914f66e46d64953182c783aee 100644
--- a/civicrm.php
+++ b/civicrm.php
@@ -2,7 +2,7 @@
 /*
 Plugin Name: CiviCRM
 Description: CiviCRM - Growing and Sustaining Relationships
-Version: 5.28.0
+Version: 5.28.1
 Requires at least: 4.9
 Requires PHP:      7.1
 Author: CiviCRM LLC
@@ -56,7 +56,7 @@ if ( ! defined( 'ABSPATH' ) ) exit;
 
 
 // Set version here: when it changes, will force JS to reload
-define( 'CIVICRM_PLUGIN_VERSION', '5.28.0' );
+define( 'CIVICRM_PLUGIN_VERSION', '5.28.1' );
 
 // Store reference to this file
 if (!defined('CIVICRM_PLUGIN_FILE')) {
diff --git a/civicrm/CRM/Activity/Form/Activity.php b/civicrm/CRM/Activity/Form/Activity.php
index afaaffac73ac6b7a2eb3fa122a4770008d9171aa..329e330960674c16ecba3fbbb74c1fcc253934ca 100644
--- a/civicrm/CRM/Activity/Form/Activity.php
+++ b/civicrm/CRM/Activity/Form/Activity.php
@@ -503,6 +503,7 @@ class CRM_Activity_Form_Activity extends CRM_Contact_Form_Task {
     }
 
     if ($this->_action & CRM_Core_Action::VIEW) {
+      $this->_values['details'] = CRM_Utils_String::purifyHtml($this->_values['details']);
       $url = CRM_Utils_System::url(implode("/", $this->urlPath), "reset=1&id={$this->_activityId}&action=view&cid={$this->_values['source_contact_id']}");
       CRM_Utils_Recent::add(CRM_Utils_Array::value('subject', $this->_values, ts('(no subject)')),
         $url,
diff --git a/civicrm/CRM/Activity/Form/Task.php b/civicrm/CRM/Activity/Form/Task.php
index c16964876815780fea5e2ed74284c25bf87d21ba..462499c38fd338b1bb705f2f9e200413cd1c96e7 100644
--- a/civicrm/CRM/Activity/Form/Task.php
+++ b/civicrm/CRM/Activity/Form/Task.php
@@ -70,16 +70,6 @@ class CRM_Activity_Form_Task extends CRM_Core_Form_Task {
       // CRM-12675
       $activityClause = NULL;
 
-      $components = CRM_Core_Component::getNames();
-      $componentClause = [];
-      foreach ($components as $componentID => $componentName) {
-        if ($componentName != 'CiviCase' && !CRM_Core_Permission::check("access $componentName")) {
-          $componentClause[] = " (activity_type.component_id IS NULL OR activity_type.component_id <> {$componentID}) ";
-        }
-      }
-      if (!empty($componentClause)) {
-        $activityClause = implode(' AND ', $componentClause);
-      }
       $result = $query->searchQuery(0, 0, NULL, FALSE, FALSE, FALSE, FALSE, FALSE, $activityClause);
 
       while ($result->fetch()) {
diff --git a/civicrm/CRM/Admin/Page/CKEditorConfig.php b/civicrm/CRM/Admin/Form/CKEditorConfig.php
similarity index 69%
rename from civicrm/CRM/Admin/Page/CKEditorConfig.php
rename to civicrm/CRM/Admin/Form/CKEditorConfig.php
index fb42b9e7b58aeece9ded68f33a30c13fc46dffe0..6de81d37ca4ace08b0a37fed8ce7c73b2d5bb00a 100644
--- a/civicrm/CRM/Admin/Page/CKEditorConfig.php
+++ b/civicrm/CRM/Admin/Form/CKEditorConfig.php
@@ -16,13 +16,9 @@
  */
 
 /**
- * Page for configuring CKEditor options.
- *
- * Note that while this is implemented as a CRM_Core_Page, it is actually a form.
- * Because the form needs to be submitted and refreshed via javascript, it seemed like
- * Quickform and CRM_Core_Form/Controller might get in the way.
+ * Form for configuring CKEditor options.
  */
-class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
+class CRM_Admin_Form_CKEditorConfig extends CRM_Core_Form {
 
   const CONFIG_FILEPATH = '[civicrm.files]/persist/crm-ckeditor-';
 
@@ -37,6 +33,7 @@ class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
     'extraPlugins',
     'toolbarGroups',
     'removeButtons',
+    'customConfig',
     'filebrowserBrowseUrl',
     'filebrowserImageBrowseUrl',
     'filebrowserFlashBrowseUrl',
@@ -45,25 +42,31 @@ class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
     'filebrowserFlashUploadUrl',
   ];
 
-  public $preset;
-
   /**
-   * Run page.
-   *
-   * @return string
+   * Prepare form
    */
-  public function run() {
-    $this->preset = CRM_Utils_Array::value('preset', $_REQUEST, 'default');
+  public function preProcess() {
+    CRM_Utils_Request::retrieve('preset', 'String', $this, FALSE, 'default', 'GET');
 
-    // If the form was submitted, take appropriate action.
-    if (!empty($_POST['revert'])) {
-      self::deleteConfigFile($this->preset);
-      self::setConfigDefault();
-    }
-    elseif (!empty($_POST['config'])) {
-      $this->save($_POST);
+    CRM_Utils_System::appendBreadCrumb([
+      [
+        'url' => CRM_Utils_System::url('civicrm/admin/setting/preferences/display', 'reset=1'),
+        'title' => ts('Display Preferences'),
+      ],
+    ]);
+
+    // Initial build
+    if (empty($_POST['qfKey'])) {
+      $this->addResources();
     }
+  }
 
+  /**
+   * Add resources during initial build or rebuild
+   *
+   * @throws CRM_Core_Exception
+   */
+  public function addResources() {
     $settings = $this->getConfigSettings();
 
     CRM_Core_Resources::singleton()
@@ -80,24 +83,67 @@ class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
         'settings' => $settings,
       ]);
 
-    $configUrl = self::getConfigUrl($this->preset) ?: self::getConfigUrl('default');
+    $configUrl = self::getConfigUrl($this->get('preset')) ?: self::getConfigUrl('default');
 
-    $this->assign('preset', $this->preset);
+    $this->assign('preset', $this->get('preset'));
     $this->assign('presets', CRM_Core_OptionGroup::values('wysiwyg_presets', FALSE, FALSE, FALSE, NULL, 'label', TRUE, FALSE, 'name'));
     $this->assign('skins', $this->getCKSkins());
     $this->assign('skin', CRM_Utils_Array::value('skin', $settings));
     $this->assign('extraPlugins', CRM_Utils_Array::value('extraPlugins', $settings));
     $this->assign('configUrl', $configUrl);
-    $this->assign('revertConfirm', htmlspecialchars(ts('Are you sure you want to revert all changes?', ['escape' => 'js'])));
+  }
 
-    CRM_Utils_System::appendBreadCrumb([
+  /**
+   * Build form
+   */
+  public function buildQuickForm() {
+    $revertConfirm = json_encode(ts('Are you sure you want to revert all changes?'));
+    $this->addButtons([
       [
-        'url' => CRM_Utils_System::url('civicrm/admin/setting/preferences/display', 'reset=1'),
-        'title' => ts('Display Preferences'),
+        'type' => 'next',
+        'name' => ts('Save'),
+      ],
+      // Hidden button used to refresh form
+      [
+        'type' => 'submit',
+        'class' => 'hiddenElement',
+        'name' => ts('Save'),
+      ],
+      [
+        'type' => 'cancel',
+        'name' => ts('Cancel'),
+      ],
+      [
+        'type' => 'refresh',
+        'name' => ts('Revert to Default'),
+        'icon' => 'fa-undo',
+        'js' => ['onclick' => "return confirm($revertConfirm);"],
       ],
     ]);
+  }
 
-    return parent::run();
+  /**
+   * Handle form submission
+   */
+  public function postProcess() {
+    if (!empty($_POST[$this->getButtonName('refresh')])) {
+      self::deleteConfigFile($this->get('preset'));
+      self::setConfigDefault();
+    }
+    else {
+      if (!empty($_POST[$this->getButtonName('next')])) {
+        $this->save($_POST);
+        CRM_Core_Session::setStatus(ts("You may need to clear your browser's cache to see the changes in CiviCRM."), ts('CKEditor Saved'), 'success');
+      }
+      // The "submit" hidden button saves but does not redirect
+      if (!empty($_POST[$this->getButtonName('submit')])) {
+        $this->save($_POST);
+        $this->addResources();
+      }
+      else {
+        CRM_Core_Session::singleton()->pushUserContext(CRM_Utils_System::url('civicrm/admin/ckeditor', ['reset' => 1]));
+      }
+    }
   }
 
   /**
@@ -110,29 +156,33 @@ class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
       // Standardize line-endings
       . preg_replace('~\R~u', "\n", $params['config']);
 
-    // Use all params starting with config_
+    // Generate a whitelist of allowed config params
+    $allOptions = json_decode(file_get_contents(\Civi::paths()->getPath('[civicrm.root]/js/wysiwyg/ck-options.json')), TRUE);
+    // These two aren't really blacklisted they're just in a different part of the form
+    $blackList = array_diff($this->blackList, ['skin', 'extraPlugins']);
+    // All options minus blacklist = whitelist
+    $whiteList = array_diff(array_column($allOptions, 'id'), $blackList);
+
+    // Save whitelisted params starting with config_
     foreach ($params as $key => $val) {
       $val = trim($val);
-      if (strpos($key, 'config_') === 0 && strlen($val)) {
+      if (strpos($key, 'config_') === 0 && strlen($val) && in_array(substr($key, 7), $whiteList)) {
         if ($val != 'true' && $val != 'false' && $val != 'null' && $val[0] != '{' && $val[0] != '[' && !is_numeric($val)) {
-          $val = json_encode($val, JSON_UNESCAPED_SLASHES);
+          $val = '"' . $val . '"';
         }
-        elseif ($val[0] == '{' || $val[0] == '[') {
-          if (!is_array(json_decode($val, TRUE))) {
-            // Invalid JSON. Do not save.
-            continue;
-          }
+        try {
+          $val = CRM_Utils_JS::encode(CRM_Utils_JS::decode($val, TRUE));
+          $pos = strrpos($config, '};');
+          $key = preg_replace('/^config_/', 'config.', $key);
+          $setting = "\n\t{$key} = {$val};\n";
+          $config = substr_replace($config, $setting, $pos, 0);
+        }
+        catch (CRM_Core_Exception $e) {
+          CRM_Core_Session::setStatus(ts("Error saving %1.", [1 => $key]), ts('Invalid Value'), 'error');
         }
-        $pos = strrpos($config, '};');
-        $key = preg_replace('/^config_/', 'config.', $key);
-        $setting = "\n\t{$key} = {$val};\n";
-        $config = substr_replace($config, $setting, $pos, 0);
       }
     }
-    self::saveConfigFile($this->preset, $config);
-    if (!empty($params['save'])) {
-      CRM_Core_Session::setStatus(ts("You may need to clear your browser's cache to see the changes in CiviCRM."), ts('CKEditor Saved'), 'success');
-    }
+    self::saveConfigFile($this->get('preset'), $config);
   }
 
   /**
@@ -190,7 +240,7 @@ class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
    */
   private function getConfigSettings() {
     $matches = $result = [];
-    $file = self::getConfigFile($this->preset) ?: self::getConfigFile('default');
+    $file = self::getConfigFile($this->get('preset')) ?: self::getConfigFile('default');
     $result['skin'] = 'moono';
     if ($file) {
       $contents = file_get_contents($file);
diff --git a/civicrm/CRM/Contact/Form/Search.php b/civicrm/CRM/Contact/Form/Search.php
index 322dc47e317bb42c42bd06f7eca146b49ce6b204..25c95ac6b7ebbb541e47d2d629604db82fad4673 100644
--- a/civicrm/CRM/Contact/Form/Search.php
+++ b/civicrm/CRM/Contact/Form/Search.php
@@ -450,6 +450,7 @@ class CRM_Contact_Form_Search extends CRM_Core_Form_Search {
         'group_contact_status', ts('Group Status')
       );
 
+      $this->assign('permissionEditSmartGroup', CRM_Core_Permission::check('edit groups'));
       $this->assign('permissionedForGroup', $permissionForGroup);
     }
 
@@ -529,6 +530,10 @@ class CRM_Contact_Form_Search extends CRM_Core_Form_Search {
     $this->_componentMode = CRM_Utils_Request::retrieve('component_mode', 'Positive', $this, FALSE, CRM_Contact_BAO_Query::MODE_CONTACTS, $_REQUEST);
     $this->_operator = CRM_Utils_Request::retrieve('operator', 'String', $this, FALSE, CRM_Contact_BAO_Query::SEARCH_OPERATOR_AND, 'REQUEST');
 
+    if (!empty($this->_ssID) && !CRM_Core_Permission::check('edit groups')) {
+      CRM_Core_Error::statusBounce(ts('You do not have permission to modify smart groups'));
+    }
+
     /**
      * set the button names
      */
diff --git a/civicrm/CRM/Contribute/Form/ContributionRecur.php b/civicrm/CRM/Contribute/Form/ContributionRecur.php
index f313d5be2237200af2ee168e7724505971f4ffb0..ce6ae68a07b552398217be900da313ea3836dd8d 100644
--- a/civicrm/CRM/Contribute/Form/ContributionRecur.php
+++ b/civicrm/CRM/Contribute/Form/ContributionRecur.php
@@ -192,21 +192,22 @@ class CRM_Contribute_Form_ContributionRecur extends CRM_Core_Form {
    */
   protected function getSubscriptionContactID() {
     $sub = $this->getSubscriptionDetails();
-    return $sub->contact_id ?? FALSE;
+    return $sub->contact_id ? (int) $sub->contact_id : FALSE;
   }
 
   /**
    * Is this being used by a front end user to update their own recurring.
    *
    * @return bool
+   * @throws \CRM_Core_Exception
    */
   protected function isSelfService() {
-    if (!is_null($this->selfService)) {
+    if ($this->selfService !== NULL) {
       return $this->selfService;
     }
     $this->selfService = FALSE;
     if (!CRM_Core_Permission::check('edit contributions')) {
-      if ($this->_subscriptionDetails->contact_id != $this->getContactID()) {
+      if ($this->getSubscriptionContactID() !== $this->getContactIDIfAccessingOwnRecord()) {
         CRM_Core_Error::statusBounce(ts('You do not have permission to cancel this recurring contribution.'));
       }
       $this->selfService = TRUE;
diff --git a/civicrm/CRM/Contribute/Page/Tab.php b/civicrm/CRM/Contribute/Page/Tab.php
index 0a5a4f67e5050380abb9a9bc2ff4349daa7ae41b..fac5a250bc7bf7907cd5913985a540ca8754268a 100644
--- a/civicrm/CRM/Contribute/Page/Tab.php
+++ b/civicrm/CRM/Contribute/Page/Tab.php
@@ -86,7 +86,11 @@ class CRM_Contribute_Page_Tab extends CRM_Core_Page {
           ];
         }
 
-        if (!$paymentProcessorObj->supports('ChangeSubscriptionAmount') && !$paymentProcessorObj->supports('EditRecurringContribution')) {
+        if (
+        (!CRM_Core_Permission::check('edit contributions') && $context === 'contribution') ||
+        (!$paymentProcessorObj->supports('ChangeSubscriptionAmount')
+          && !$paymentProcessorObj->supports('EditRecurringContribution')
+        )) {
           unset($links[CRM_Core_Action::UPDATE]);
         }
       }
diff --git a/civicrm/CRM/Core/Form.php b/civicrm/CRM/Core/Form.php
index 7bd57e0dd7a0722496ae5adf59b66825f1ef1e06..17b30a99865a0222dcb969ac5940fd9114973338 100644
--- a/civicrm/CRM/Core/Form.php
+++ b/civicrm/CRM/Core/Form.php
@@ -2219,11 +2219,13 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
 
   /**
    * Get the contact id of the logged in user.
+   *
+   * @return int|false
    */
   public function getLoggedInUserContactID() {
     // check if the user is logged in and has a contact ID
     $session = CRM_Core_Session::singleton();
-    return $session->get('userID');
+    return $session->get('userID') ? (int) $session->get('userID') : FALSE;
   }
 
   /**
@@ -2247,6 +2249,7 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    *   - id_field
    *   - url (for ajax lookup)
    *
+   * @throws \CRM_Core_Exception
    * @todo add data attributes so we can deal with multiple instances on a form
    */
   public function addAutoSelector($profiles = [], $autoCompleteField = []) {
@@ -2637,4 +2640,26 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     }
   }
 
+  /**
+   * Get the contact if from the url, using the checksum or the cid if it is the logged in user.
+   *
+   * This function returns the user being validated. It is not intended to get another user
+   * they have permission to (setContactID does do that) and can be used to check if the user is
+   * accessing their own record.
+   *
+   * @return int|false
+   * @throws \CRM_Core_Exception
+   */
+  protected function getContactIDIfAccessingOwnRecord() {
+    $contactID = (int) CRM_Utils_Request::retrieve('cid', 'Positive', $this);
+    if (!$contactID) {
+      return FALSE;
+    }
+    if ($contactID === $this->getLoggedInUserContactID()) {
+      return $contactID;
+    }
+    $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
+    return CRM_Contact_BAO_Contact_Utils::validChecksum($contactID, $userChecksum) ? $contactID : FALSE;
+  }
+
 }
diff --git a/civicrm/CRM/Core/I18n/SchemaStructure.php b/civicrm/CRM/Core/I18n/SchemaStructure.php
index ecadf83929c92afe8751d3ef86a9ba1f984827ae..02a51c9a7874743830aacb42da5238c55b6519fa 100644
--- a/civicrm/CRM/Core/I18n/SchemaStructure.php
+++ b/civicrm/CRM/Core/I18n/SchemaStructure.php
@@ -154,7 +154,7 @@ class CRM_Core_I18n_SchemaStructure {
           'help_post' => "text COMMENT 'Description and/or help text to display after this field.'",
         ],
         'civicrm_price_field_value' => [
-          'label' => "varchar(255) NOT NULL COMMENT 'Price field option label'",
+          'label' => "varchar(255) DEFAULT NULL COMMENT 'Price field option label'",
           'description' => "text DEFAULT NULL COMMENT 'Price field option description.'",
           'help_pre' => "text DEFAULT NULL COMMENT 'Price field option pre help text.'",
           'help_post' => "text DEFAULT NULL COMMENT 'Price field option post field help.'",
@@ -586,7 +586,6 @@ class CRM_Core_I18n_SchemaStructure {
         'civicrm_price_field_value' => [
           'label' => [
             'type' => "Text",
-            'required' => "true",
           ],
           'description' => [
             'type' => "TextArea",
diff --git a/civicrm/CRM/Core/Key.php b/civicrm/CRM/Core/Key.php
index 83317ac14946e180f0fa8f5d0c9420b7a5f3b4c8..05cc58a541410d2df16830a37de12e2e1ac6cc8c 100644
--- a/civicrm/CRM/Core/Key.php
+++ b/civicrm/CRM/Core/Key.php
@@ -17,6 +17,28 @@
  *
  */
 class CRM_Core_Key {
+
+  /**
+   * The length of the randomly-generated, per-session signing key.
+   *
+   * Expressed as number of bytes. (Ex: 128 bits = 16 bytes)
+   *
+   * @var int
+   */
+  const PRIVATE_KEY_LENGTH = 16;
+
+  /**
+   * @var string
+   * @see hash_hmac_algos()
+   */
+  const HASH_ALGO = 'sha256';
+
+  /**
+   * The length of a generated signature/digest (expressed in hex digits).
+   * @var int
+   */
+  const HASH_LENGTH = 64;
+
   public static $_key = NULL;
 
   public static $_sessionID = NULL;
@@ -32,7 +54,7 @@ class CRM_Core_Key {
       $session = CRM_Core_Session::singleton();
       self::$_key = $session->get('qfPrivateKey');
       if (!self::$_key) {
-        self::$_key = md5(uniqid(mt_rand(), TRUE)) . md5(uniqid(mt_rand(), TRUE));
+        self::$_key = base64_encode(random_bytes(self::PRIVATE_KEY_LENGTH));
         $session->set('qfPrivateKey', self::$_key);
       }
     }
@@ -66,9 +88,7 @@ class CRM_Core_Key {
    *   valid formID
    */
   public static function get($name, $addSequence = FALSE) {
-    $privateKey = self::privateKey();
-    $sessionID = self::sessionID();
-    $key = md5($sessionID . $name . $privateKey);
+    $key = self::sign($name);
 
     if ($addSequence) {
       // now generate a random number between 1 and 100K and add it to the key
@@ -103,9 +123,7 @@ class CRM_Core_Key {
       $k = $key;
     }
 
-    $privateKey = self::privateKey();
-    $sessionID = self::sessionID();
-    if ($k != md5($sessionID . $name . $privateKey)) {
+    if (!hash_equals($k, self::sign($name))) {
       return NULL;
     }
     return $key;
@@ -115,9 +133,10 @@ class CRM_Core_Key {
    * @param $key
    *
    * @return bool
+   *   TRUE if the signature ($key) is well-formed.
    */
   public static function valid($key) {
-    // a valid key is a 32 digit hex number
+    // a valid key is a hex number
     // followed by an optional _ and a number between 1 and 10000
     if (strpos('_', $key) !== FALSE) {
       list($hash, $seq) = explode('_', $key);
@@ -134,8 +153,26 @@ class CRM_Core_Key {
       $hash = $key;
     }
 
-    // ensure that hash is a 32 digit hex number
-    return (bool) preg_match('#[0-9a-f]{32}#i', $hash);
+    // ensure that hash is a hex number (of expected length)
+    return preg_match('#[0-9a-f]{' . self::HASH_LENGTH . '}#i', $hash) ? TRUE : FALSE;
+  }
+
+  /**
+   * @param string $name
+   *   The name of the form
+   * @return string
+   *   A signed digest of $name, computed with the per-session private key
+   */
+  private static function sign($name) {
+    $privateKey = self::privateKey();
+    $sessionID = self::sessionID();
+    $delim = chr(0);
+    if (strpos($sessionID, $delim) !== FALSE || strpos($name, $delim) !== FALSE) {
+      throw new \RuntimeException("Failed to generate signature. Malformed session-id or form-name.");
+    }
+    // Note: Unsure why $sessionID is included, but it's always been there, and it doesn't seem harmful.
+    return hash_hmac(self::HASH_ALGO, $sessionID . $delim . $name, $privateKey);
+
   }
 
 }
diff --git a/civicrm/CRM/Core/LegacyErrorHandler.php b/civicrm/CRM/Core/LegacyErrorHandler.php
index f82b3ce6d3d2f9f2b72ff42be5118f085d54fc5c..de515ecee12ec0d5bd6ef988e742e1c24a3738b6 100644
--- a/civicrm/CRM/Core/LegacyErrorHandler.php
+++ b/civicrm/CRM/Core/LegacyErrorHandler.php
@@ -16,9 +16,9 @@ class CRM_Core_LegacyErrorHandler {
       $message = $e->getMessage();
       $session = CRM_Core_Session::singleton();
       $session->setStatus(
-        $message,
-        CRM_Utils_Array::value('message_title', $params),
-        CRM_Utils_Array::value('message_type', $params, 'error')
+        htmlspecialchars($message),
+        htmlspecialchars($params['message_title'] ?? ts('Error')),
+        $params['message_type'] ?? 'error'
       );
     }
   }
diff --git a/civicrm/CRM/Core/Payment.php b/civicrm/CRM/Core/Payment.php
index 9c998dc66d604dd2dde9ee5c848cd572f2068688..b312c8e8d9318fe1d12426f75638cb074bb8c686 100644
--- a/civicrm/CRM/Core/Payment.php
+++ b/civicrm/CRM/Core/Payment.php
@@ -831,7 +831,7 @@ abstract class CRM_Core_Payment {
           'size' => 20,
           'maxlength' => 20,
           'autocomplete' => 'off',
-          'class' => 'creditcard',
+          'class' => 'creditcard required',
         ],
         'is_required' => TRUE,
         // 'description' => '16 digit card number', // If you enable a description field it will be shown below the field on the form
@@ -844,6 +844,7 @@ abstract class CRM_Core_Payment {
           'size' => 5,
           'maxlength' => 10,
           'autocomplete' => 'off',
+          'class' => ($isCVVRequired ? 'required' : ''),
         ],
         'is_required' => $isCVVRequired,
         'rules' => [
@@ -867,7 +868,7 @@ abstract class CRM_Core_Payment {
             'rule_parameters' => TRUE,
           ],
         ],
-        'extra' => ['class' => 'crm-form-select'],
+        'extra' => ['class' => 'crm-form-select required'],
       ],
       'credit_card_type' => [
         'htmlType' => 'select',
@@ -884,6 +885,7 @@ abstract class CRM_Core_Payment {
           'size' => 20,
           'maxlength' => 34,
           'autocomplete' => 'on',
+          'class' => 'required',
         ],
         'is_required' => TRUE,
       ],
@@ -896,6 +898,7 @@ abstract class CRM_Core_Payment {
           'size' => 20,
           'maxlength' => 34,
           'autocomplete' => 'off',
+          'class' => 'required',
         ],
         'rules' => [
           [
@@ -915,6 +918,7 @@ abstract class CRM_Core_Payment {
           'size' => 20,
           'maxlength' => 11,
           'autocomplete' => 'off',
+          'class' => 'required',
         ],
         'is_required' => TRUE,
         'rules' => [
@@ -933,6 +937,7 @@ abstract class CRM_Core_Payment {
           'size' => 20,
           'maxlength' => 64,
           'autocomplete' => 'off',
+          'class' => 'required',
         ],
         'is_required' => TRUE,
 
@@ -1034,6 +1039,7 @@ abstract class CRM_Core_Payment {
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
+        'class' => 'required',
       ],
       'is_required' => TRUE,
     ];
@@ -1060,6 +1066,7 @@ abstract class CRM_Core_Payment {
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
+        'class' => 'required',
       ],
       'is_required' => TRUE,
     ];
@@ -1073,6 +1080,7 @@ abstract class CRM_Core_Payment {
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
+        'class' => 'required',
       ],
       'is_required' => TRUE,
     ];
@@ -1086,6 +1094,7 @@ abstract class CRM_Core_Payment {
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
+        'class' => 'required',
       ],
       'is_required' => TRUE,
     ];
@@ -1096,6 +1105,7 @@ abstract class CRM_Core_Payment {
       'name' => "billing_state_province_id-{$billingLocationID}",
       'cc_field' => TRUE,
       'is_required' => TRUE,
+      'extra' => ['class' => 'required'],
     ];
 
     $metadata["billing_postal_code-{$billingLocationID}"] = [
@@ -1107,6 +1117,7 @@ abstract class CRM_Core_Payment {
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
+        'class' => 'required',
       ],
       'is_required' => TRUE,
     ];
@@ -1120,6 +1131,7 @@ abstract class CRM_Core_Payment {
         '' => ts('- select -'),
       ] + CRM_Core_PseudoConstant::country(),
       'is_required' => TRUE,
+      'extra' => ['class' => 'required'],
     ];
     return $metadata;
   }
diff --git a/civicrm/CRM/Core/Payment/Form.php b/civicrm/CRM/Core/Payment/Form.php
index 3779297a08e08327f64fa2519a97ab0e98e319af..1c7263812c522bae496812da1b71964e17012560 100644
--- a/civicrm/CRM/Core/Payment/Form.php
+++ b/civicrm/CRM/Core/Payment/Form.php
@@ -109,7 +109,7 @@ class CRM_Core_Payment_Form {
           $field['name'],
           $field['title'],
           $field['attributes'],
-          $field['is_required'],
+          FALSE,
           $field['extra']
         );
       }
diff --git a/civicrm/CRM/Core/Resources.php b/civicrm/CRM/Core/Resources.php
index 40dd3b09782ff88268e34fcffeddc28af162ed90..0fdc57cf21ded218925ae180b11156b934f0e5e8 100644
--- a/civicrm/CRM/Core/Resources.php
+++ b/civicrm/CRM/Core/Resources.php
@@ -760,11 +760,11 @@ class CRM_Core_Resources {
     // add wysiwyg editor
     $editor = Civi::settings()->get('editor_id');
     if ($editor == "CKEditor") {
-      CRM_Admin_Page_CKEditorConfig::setConfigDefault();
+      CRM_Admin_Form_CKEditorConfig::setConfigDefault();
       $items[] = [
         'config' => [
           'wysisygScriptLocation' => Civi::paths()->getUrl("[civicrm.root]/js/wysiwyg/crm.ckeditor.js"),
-          'CKEditorCustomConfig' => CRM_Admin_Page_CKEditorConfig::getConfigUrl(),
+          'CKEditorCustomConfig' => CRM_Admin_Form_CKEditorConfig::getConfigUrl(),
         ],
       ];
     }
diff --git a/civicrm/CRM/Core/Task.php b/civicrm/CRM/Core/Task.php
index 40985c4b451b6b99720b049adf1fe584d488ad52..a947f4729c001b9fd6fb7cfdbd10d770ed01a87e 100644
--- a/civicrm/CRM/Core/Task.php
+++ b/civicrm/CRM/Core/Task.php
@@ -132,7 +132,7 @@ abstract class CRM_Core_Task {
    */
   public static function corePermissionedTaskTitles($tasks, $permission, $params) {
     // Only offer the "Update Smart Group" task if a smart group/saved search is already in play and we have edit permissions
-    if (!empty($params['ssID']) && ($permission == CRM_Core_Permission::EDIT)) {
+    if (!empty($params['ssID']) && ($permission == CRM_Core_Permission::EDIT) && CRM_Core_Permission::check('edit groups')) {
       $tasks[self::SAVE_SEARCH_UPDATE] = self::$_tasks[self::SAVE_SEARCH_UPDATE]['title'];
     }
     else {
diff --git a/civicrm/CRM/Core/xml/Menu/Admin.xml b/civicrm/CRM/Core/xml/Menu/Admin.xml
index acf56251f115fccd4a34b6bf21dc4f5129d96b32..20b52451cc27c20c438e6643e304fe25000f42c8 100644
--- a/civicrm/CRM/Core/xml/Menu/Admin.xml
+++ b/civicrm/CRM/Core/xml/Menu/Admin.xml
@@ -670,7 +670,7 @@
   <item>
     <path>civicrm/admin/ckeditor</path>
     <title>Configure CKEditor</title>
-    <page_callback>CRM_Admin_Page_CKEditorConfig</page_callback>
+    <page_callback>CRM_Admin_Form_CKEditorConfig</page_callback>
     <access_arguments>administer CiviCRM</access_arguments>
   </item>
 </menu>
diff --git a/civicrm/CRM/Dedupe/Merger.php b/civicrm/CRM/Dedupe/Merger.php
index 74669495b4e596984be8ff630f6bdf19f0b50194..86217e04979f2ce3cab3c65b74aa5dd4cec04f92 100644
--- a/civicrm/CRM/Dedupe/Merger.php
+++ b/civicrm/CRM/Dedupe/Merger.php
@@ -541,6 +541,17 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
         continue;
       }
 
+      if ($table === 'civicrm_setting') {
+        // Per https://lab.civicrm.org/dev/core/-/issues/1934
+        // Note this line is not unit tested as yet as a quick-fix for a regression
+        // but it would be better to do a SELECT request & only update if needed (as a general rule
+        // more selects & less UPDATES will result in less deadlocks while de-duping.
+        // Note the delete is not important here - it can stay with the deleted contact on the
+        // off chance they get restored.
+        $sqls[] = "UPDATE IGNORE civicrm_setting SET contact_id = $mainId WHERE contact_id = $otherId";
+        continue;
+      }
+
       // use UPDATE IGNORE + DELETE query pair to skip on situations when
       // there's a UNIQUE restriction on ($field, some_other_field) pair
       if (isset($cidRefs[$table])) {
diff --git a/civicrm/CRM/Price/DAO/PriceFieldValue.php b/civicrm/CRM/Price/DAO/PriceFieldValue.php
index d510c14b62d2932e308347a3aae70f5e07a1b0b7..01aff179797e27985e54b00c84879293489a1e71 100644
--- a/civicrm/CRM/Price/DAO/PriceFieldValue.php
+++ b/civicrm/CRM/Price/DAO/PriceFieldValue.php
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Price/PriceFieldValue.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:28432a14b1b1523380eb41e8e481037d)
+ * (GenCodeChecksum:11a02f3576be10e8c2a0ea47a19e2dac)
  */
 
 /**
@@ -226,10 +226,10 @@ class CRM_Price_DAO_PriceFieldValue extends CRM_Core_DAO {
           'type' => CRM_Utils_Type::T_STRING,
           'title' => ts('Name'),
           'description' => ts('Price field option name'),
-          'required' => TRUE,
           'maxlength' => 255,
           'size' => CRM_Utils_Type::HUGE,
           'where' => 'civicrm_price_field_value.name',
+          'default' => 'NULL',
           'table_name' => 'civicrm_price_field_value',
           'entity' => 'PriceFieldValue',
           'bao' => 'CRM_Price_BAO_PriceFieldValue',
@@ -244,10 +244,10 @@ class CRM_Price_DAO_PriceFieldValue extends CRM_Core_DAO {
           'type' => CRM_Utils_Type::T_STRING,
           'title' => ts('Label'),
           'description' => ts('Price field option label'),
-          'required' => TRUE,
           'maxlength' => 255,
           'size' => CRM_Utils_Type::HUGE,
           'where' => 'civicrm_price_field_value.label',
+          'default' => 'NULL',
           'table_name' => 'civicrm_price_field_value',
           'entity' => 'PriceFieldValue',
           'bao' => 'CRM_Price_BAO_PriceFieldValue',
diff --git a/civicrm/CRM/Report/Form.php b/civicrm/CRM/Report/Form.php
index 473356912470882735db7964352d705c8fe713e1..2bd8d4d5069bbc8019b1c12d65f0594864c9ea5e 100644
--- a/civicrm/CRM/Report/Form.php
+++ b/civicrm/CRM/Report/Form.php
@@ -139,11 +139,6 @@ class CRM_Report_Form extends CRM_Core_Form {
    */
   protected $_groupFilter = FALSE;
 
-  /**
-   * Required for civiexportexcel.
-   */
-  public $supportsExportExcel = TRUE;
-
   /**
    * Has the report been optimised for group filtering.
    *
@@ -1440,7 +1435,7 @@ class CRM_Report_Form extends CRM_Core_Form {
     if (!CRM_Core_Permission::check('view report sql')) {
       return;
     }
-    $ignored_output_modes = ['pdf', 'csv', 'print', 'excel2007'];
+    $ignored_output_modes = ['pdf', 'csv', 'print'];
     if (in_array($this->_outputMode, $ignored_output_modes)) {
       return;
     }
@@ -2866,11 +2861,6 @@ WHERE cg.extends IN ('" . implode("','", $this->_customGroupExtends) . "') AND
       $this->_absoluteUrl = TRUE;
       $this->addPaging = FALSE;
     }
-    elseif ($this->_outputMode == 'excel2007') {
-      $printOnly = TRUE;
-      $this->_absoluteUrl = TRUE;
-      $this->addPaging = FALSE;
-    }
     elseif ($this->_outputMode == 'copy' && $this->_criteriaForm) {
       $this->_createNew = TRUE;
     }
@@ -3508,9 +3498,6 @@ WHERE cg.extends IN ('" . implode("','", $this->_customGroupExtends) . "') AND
     elseif ($this->_outputMode == 'csv') {
       CRM_Report_Utils_Report::export2csv($this, $rows);
     }
-    elseif ($this->_outputMode == 'excel2007') {
-      CRM_CiviExportExcel_Utils_Report::export2excel2007($this, $rows);
-    }
     elseif ($this->_outputMode == 'group') {
       $group = $this->_params['groups'];
       $this->add2group($group);
diff --git a/civicrm/CRM/Upgrade/Incremental/php/FiveTwentyEight.php b/civicrm/CRM/Upgrade/Incremental/php/FiveTwentyEight.php
index b035cfb16e3e7e501833bf39fe68db321a28dd72..01c899a7dedcfa168236ada4b93aab9d6228acca 100644
--- a/civicrm/CRM/Upgrade/Incremental/php/FiveTwentyEight.php
+++ b/civicrm/CRM/Upgrade/Incremental/php/FiveTwentyEight.php
@@ -46,6 +46,37 @@ class CRM_Upgrade_Incremental_php_FiveTwentyEight extends CRM_Upgrade_Incrementa
     }
   }
 
+  /**
+   * Upgrade function.
+   *
+   * @param string $rev
+   */
+  public function upgrade_5_28_1($rev) {
+    $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
+    $this->addTask('Make label field non required on price field value', 'priceFieldValueLabelNonRequired');
+  }
+
+  /**
+   * Make the price field value label column non required
+   * @return bool
+   */
+  public static function priceFieldValueLabelNonRequired() {
+    $domain = new CRM_Core_DAO_Domain();
+    $domain->find(TRUE);
+    if ($domain->locales) {
+      $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
+      foreach ($locales as $locale) {
+        CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_price_field_value CHANGE `label_{$locale}` `label_{$locale}` varchar(255) DEFAULT NULL  COMMENT 'Price field option label'", [], TRUE, NULL, FALSE, FALSE);
+        CRM_Core_DAO::executeQuery("UPDATE civicrm_price_field_value SET label_{$locale} = NULL WHERE label_{$locale} = 'null'", [], TRUE, NULL, FALSE, FALSE);
+      }
+    }
+    else {
+      CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_price_field_value CHANGE `label` `label` varchar(255) DEFAULT NULL  COMMENT 'Price field option label'", [], TRUE, NULL, FALSE, FALSE);
+      CRM_Core_DAO::executeQuery("UPDATE civicrm_price_field_value SET label = NULL WHERE label = 'null'", [], TRUE, NULL, FALSE, FALSE);
+    }
+    return TRUE;
+  }
+
   public static function createWpFilesMessage() {
     if (!function_exists('civi_wp')) {
       return '';
diff --git a/civicrm/CRM/Upgrade/Incremental/sql/5.28.1.mysql.tpl b/civicrm/CRM/Upgrade/Incremental/sql/5.28.1.mysql.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..46fa031fe5eb1061dbefaa23b4794866c0d37507
--- /dev/null
+++ b/civicrm/CRM/Upgrade/Incremental/sql/5.28.1.mysql.tpl
@@ -0,0 +1 @@
+{* file to handle db changes in 5.28.1 during upgrade *}
diff --git a/civicrm/CRM/Utils/JS.php b/civicrm/CRM/Utils/JS.php
index 27e52d0d1ffb0c86acaaa11ecd4a949654563178..da228d24136a30c4057f2a8468f324a55e946824 100644
--- a/civicrm/CRM/Utils/JS.php
+++ b/civicrm/CRM/Utils/JS.php
@@ -125,26 +125,51 @@ class CRM_Utils_JS {
    * ]
    *
    * @param string $js
+   * @param bool $throwException
    * @return mixed
-   * @throws Exception
+   * @throws CRM_Core_Exception
    */
-  public static function decode($js) {
+  public static function decode($js, $throwException = FALSE) {
     $js = trim($js);
     $first = substr($js, 0, 1);
     $last = substr($js, -1);
-    if ($last === $first && ($first === "'" || $first === '"')) {
-      // Use a temp placeholder for escaped backslashes
-      $backslash = chr(0) . 'backslash' . chr(0);
-      return str_replace(['\\\\', "\\'", '\\"', '\\&', '\\/', $backslash], [$backslash, "'", '"', '&', '/', '\\'], substr($js, 1, -1));
+    if ($first === "'" && $last === "'") {
+      $js = self::convertSingleQuoteString($js, $throwException);
     }
-    if (($first === '{' && $last === '}') || ($first === '[' && $last === ']')) {
+    elseif (($first === '{' && $last === '}') || ($first === '[' && $last === ']')) {
       $obj = self::getRawProps($js);
       foreach ($obj as $idx => $item) {
-        $obj[$idx] = self::decode($item);
+        $obj[$idx] = self::decode($item, $throwException);
       }
       return $obj;
     }
-    return json_decode($js);
+    $result = json_decode($js);
+    if ($throwException && $result === NULL && $js !== 'null') {
+      throw new CRM_Core_Exception(json_last_error_msg());
+    }
+    return $result;
+  }
+
+  /**
+   * @param string $str
+   * @return string|null
+   * @throws CRM_Core_Exception
+   */
+  public static function convertSingleQuoteString(string $str, $throwException) {
+    // json_decode can only handle double quotes around strings, so convert single-quoted strings
+    $backslash = chr(0) . 'backslash' . chr(0);
+    $str = str_replace(['\\\\', '\\"', '"', '\\&', '\\/', $backslash], [$backslash, '"', '\\"', '&', '/', '\\'], substr($str, 1, -1));
+    // Ensure the string doesn't terminate early by checking that all single quotes are escaped
+    $pos = -1;
+    while (($pos = strpos($str, "'", $pos + 1)) !== FALSE) {
+      if (($pos - strlen(rtrim(substr($str, 0, $pos)))) % 2) {
+        if ($throwException) {
+          throw new CRM_Core_Exception('Invalid string passed to CRM_Utils_JS::decode');
+        }
+        return NULL;
+      }
+    }
+    return '"' . $str . '"';
   }
 
   /**
diff --git a/civicrm/Civi/Api4/Service/Spec/Provider/PriceFieldValueCreationSpecProvider.php b/civicrm/Civi/Api4/Service/Spec/Provider/PriceFieldValueCreationSpecProvider.php
index 00c6b19d1e6c8952e0cb0ce32d98cecbd077b5ea..76cfa28739ca56efc33468c96fb543465ce4b09f 100644
--- a/civicrm/Civi/Api4/Service/Spec/Provider/PriceFieldValueCreationSpecProvider.php
+++ b/civicrm/Civi/Api4/Service/Spec/Provider/PriceFieldValueCreationSpecProvider.php
@@ -29,6 +29,8 @@ class PriceFieldValueCreationSpecProvider implements Generic\SpecProviderInterfa
   public function modifySpec(RequestSpec $spec) {
     // Name will be auto-generated from label if not supplied
     $spec->getFieldByName('name')->setRequired(FALSE);
+    // Ensure that label is required this matches v3 API but doesn't match DAO because form fields allow for NULLs
+    $spec->getFieldByName('label')->setRequired(TRUE);
   }
 
   /**
diff --git a/civicrm/civicrm-version.php b/civicrm/civicrm-version.php
index d9b0934df78d2053483ca866d2574a5e977dd27e..948a2c6b5f6b35fe8d2516a3b9b8d6d456d77ecb 100644
--- a/civicrm/civicrm-version.php
+++ b/civicrm/civicrm-version.php
@@ -1,7 +1,7 @@
 <?php
 /** @deprecated */
 function civicrmVersion( ) {
-  return array( 'version'  => '5.28.0',
+  return array( 'version'  => '5.28.1',
                 'cms'      => 'Wordpress',
                 'revision' => '' );
 }
diff --git a/civicrm/js/Common.js b/civicrm/js/Common.js
index 0199e80eb1c5c37574a5699cbf4b8acd53f69f3d..e6b0dbc1aacf9a38726846cf58f36239dae07a18 100644
--- a/civicrm/js/Common.js
+++ b/civicrm/js/Common.js
@@ -1659,4 +1659,92 @@ if (!CRM.vars) CRM.vars = {};
     }
   });
 
+  // CVE-2020-11022 and CVE-2020-11023  Passing HTML from untrusted sources - even after sanitizing it - to one of jQuery's DOM manipulation methods (i.e. .html(), .append(), and others) may execute untrusted code.
+  $.htmlPrefilter = function(html) {
+    // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
+    // their XML equivalent: e.g., "<div />" to "<div></div>". This is
+    // problematic for several reasons, including that it's vulnerable to XSS
+    // attacks. However, since this was jQuery's behavior for many years, many
+    // Drupal modules and jQuery plugins may be relying on it. Therefore, we
+    // preserve that behavior, but for a limited set of tags only, that we believe
+    // to not be vulnerable. This is the set of HTML tags that satisfy all of the
+    // following conditions:
+    // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
+    //   appear in that list, then we don't want to mess with it here either.
+    //   @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
+    // - A normal element (not a void, template, text, or foreign element).
+    //   @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
+    // - An element that is still defined by the current HTML specification
+    //   (not a deprecated element), because we do not want to rely on how
+    //   browsers parse deprecated elements.
+    //   @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
+    // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
+    //   designed for fragments, not entire documents.
+    // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
+    //   regular expression, it didn't match on colgroup, and we don't want to
+    //   introduce a behavior change for that.
+    var selfClosingTagsToReplace = [
+      'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
+      'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
+      'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
+      'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
+      'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
+      'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
+      'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
+      'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
+      'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
+      'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
+    ];
+
+    // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
+    // two expressions makes it easier to target <a/> without also targeting
+    // every tag that starts with "a".
+    var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
+    var whitespace = '[\\x20\\t\\r\\n\\f]';
+    var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
+    var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
+
+    // jQuery 3.5 also fixed a vulnerability for when </select> appears within
+    // an <option> or <optgroup>, but it did that in local code that we can't
+    // backport directly. Instead, we filter such cases out. To do so, we need to
+    // determine when jQuery would otherwise invoke the vulnerable code, which it
+    // uses this regular expression to determine. The regular expression changed
+    // for version 3.0.0 and changed again for 3.4.0.
+    // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
+    // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
+    // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
+    var rtagName = /<([\w:]+)/;
+
+    // The regular expression that jQuery uses to determine which self-closing
+    // tags to expand to open and close tags. This is vulnerable, because it
+    // matches all tag names except the few excluded ones. We only use this
+    // expression for determining vulnerability. The expression changed for
+    // version 3, but we only need to check for vulnerability in versions 1 and 2,
+    // so we use the expression from those versions.
+    // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
+    var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
+
+    // This is how jQuery determines the first tag in the HTML.
+    // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
+    var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
+
+    // It is not valid HTML for <option> or <optgroup> to have <select> as
+    // either a descendant or sibling, and attempts to inject one can cause
+    // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
+    // possible XSS attack, reject the entire string.
+    // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
+    if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
+      html = '';
+    }
+
+    // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
+    // the tags in the `selfClosingTagsToReplace` list defined above.
+    // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
+    // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
+    html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
+    html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
+
+    return html;
+  };
+
 })(jQuery, _);
diff --git a/civicrm/js/wysiwyg/admin.ckeditor-configurator.js b/civicrm/js/wysiwyg/admin.ckeditor-configurator.js
index 6c44806754b7628f144356f1aeac4fb869401c76..9d87afc0dc20b02d6edf80a7fad86be336bec3d1 100644
--- a/civicrm/js/wysiwyg/admin.ckeditor-configurator.js
+++ b/civicrm/js/wysiwyg/admin.ckeditor-configurator.js
@@ -65,15 +65,7 @@
   }
 
   function validateJson() {
-    var val = $(this).val();
-    $(this).parent().removeClass('crm-error');
-    if (val[0] === '[' || val[0] === '{') {
-      try {
-        JSON.parse(val);
-      } catch (e) {
-        $(this).parent().addClass('crm-error');
-      }
-    }
+    // TODO: strict json isn't required so we can't use JSON.parse for error checking. Need something like angular.eval.
   }
 
   function addOption() {
@@ -109,7 +101,7 @@
     var selectorOpen = false,
       changedWhileOpen = false;
 
-    $('#toolbarModifierForm')
+    $('#CKEditorConfig')
       .on('submit', function(e) {
         $('.toolbar button:last', '#toolbarModifierWrapper')[0].click();
         $('.configContainer textarea', '#toolbarModifierWrapper').attr('name', 'config');
@@ -117,7 +109,8 @@
       .on('change', '.config-param', function(e) {
         changedWhileOpen = true;
         if (!selectorOpen) {
-          $('#toolbarModifierForm').submit().block();
+          $('#_qf_CKEditorConfig_submit-bottom').click();
+          $('#CKEditorConfig').block();
         }
       })
       .on('change', 'input.crm-config-option-name', changeOptionName)
diff --git a/civicrm/release-notes.md b/civicrm/release-notes.md
index a7c7f0ef3554e82154357e519d1f0316b78dc630..d4ea4c09abb3794e17ce3a6b93c3317bf20546b2 100644
--- a/civicrm/release-notes.md
+++ b/civicrm/release-notes.md
@@ -15,6 +15,16 @@ Other resources for identifying changes are:
     * https://github.com/civicrm/civicrm-joomla
     * https://github.com/civicrm/civicrm-wordpress
 
+## CiviCRM 5.28.1
+
+Released August 19, 2020
+
+- **[Synopsis](release-notes/5.28.1.md#synopsis)**
+- **[Security advisories](release-notes/5.28.1.md#security)**
+- **[Bugs resolved](release-notes/5.28.1.md#bugs)**
+- **[Credits](release-notes/5.28.1.md#credits)**
+- **[Feedback](release-notes/5.28.1.md#feedback)**
+
 ## CiviCRM 5.28.0
 
 Released August 5, 2020
diff --git a/civicrm/release-notes/5.28.1.md b/civicrm/release-notes/5.28.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..5c85559bbfcf0172905d43780a828095e2fd4458
--- /dev/null
+++ b/civicrm/release-notes/5.28.1.md
@@ -0,0 +1,58 @@
+# CiviCRM 5.28.1
+
+Released August 19, 2020
+
+- **[Security advisories](#security)**
+- **[Bugs Resolved](#bugs)**
+- **[Credits](#credits)**
+
+## <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-2020-09](https://civicrm.org/advisory/civi-sa-2020-09-privilege-escalation-acl-smart-groups): Privilege Escalation via Smart Groups**
+- **[CIVI-SA-2020-10](https://civicrm.org/advisory/civi-sa-2020-10-cross-site-scripting-activity-details): Cross Site Scripting in Activity Details**
+- **[CIVI-SA-2020-11](https://civicrm.org/advisory/civi-sa-2020-11-csrf-ckeditor-configuration-form): CSRF on CKEditor Configuration**
+- **[CIVI-SA-2020-12](https://civicrm.org/advisory/civi-sa-2020-12-xss-ckeditor-configuration): XSS in CKEditor Configuration**
+- **[CIVI-SA-2020-13](https://civicrm.org/advisory/civi-sa-2020-13-xss-event-summary): XSS in Event Summary**
+- **[CIVI-SA-2020-14](https://civicrm.org/advisory/civi-sa-2020-14-xss-profile-description-field): XSS in Profile Description**
+- **[CIVI-SA-2020-15](https://civicrm.org/advisory/civi-sa-2020-15-persistent-xss-contact-activity-tab): Persistant XSS in Contact Activity Tab**
+- **[CIVI-SA-2020-16](https://civicrm.org/advisory/civi-sa-2020-16-jquery-security-update-cve-2020-11022-cve-2020-11023): jQuery CVE-202-11022, CVE-2020-11023**
+- **[CIVI-SA-2020-17](https://civicrm.org/advisory/civi-sa-2020-17-harden-session-private-key): Harden Per-Session Private Key**
+- **[CIVI-SA-2020-18](https://civicrm.org/advisory/civi-sa-2020-18-html-injection-through-error-message): HTML Injection via Error Message**
+- **[CIVI-SA-2020-19](https://civicrm.org/advisory/civi-sa-2020-19-edit-permission-recurring-contributions): Edit Permission for Recurring Contributions**
+
+## <a name="bugs"></a>Bugs Resolved
+
+* **_Activities_: Exporting all activities from a "Find Activity" search as an ACLed user causes DB error ([dev/core#1952](https://lab.civicrm.org/dev/core/-/issues/1952):
+  [#18017](https://github.com/civicrm/civicrm-core/pull/18017))**
+* **_CiviContribute_: Receipts display unlabeled price options as "null" ([dev/core#1936](https://lab.civicrm.org/dev/core/-/issues/1936):
+  [#18124](https://github.com/civicrm/civicrm-core/pull/18124))**
+* **_CiviContribute_: Credit card fields are required even when the amount is 0 ([dev/core#1953](https://lab.civicrm.org/dev/core/-/issues/1953):
+  [#18144](https://github.com/civicrm/civicrm-core/pull/18144), [#16163](https://github.com/civicrm/civicrm-core/pull/16163), [#18166](https://github.com/civicrm/civicrm-core/pull/16166))**
+* **_Dedupe_: Merging contacts with certain "Settings" produces error ([dev/core#1934](https://lab.civicrm.org/dev/core/-/issues/1934):
+  [#18126](https://github.com/civicrm/civicrm-core/pull/18126))**
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following people, who participated in
+various stages of reporting, analysis, development, review, and testing:
+
+Ben Hubbard - Armadillo Security; Coleman Watts - CiviCRM; Cure53; Dave D;
+Dennis Brinkrolf - RIPS Technologies; Eileen McNaughton - Wikipedia
+Foundation; Jamie Novick - Compucorp; Jens Schuppe; Jude Hungerford - Asylum
+Seekers Center; Karin Gerritsen - Semper IT; Kevin Cristiano - Tadpole
+Collective; Mark Rogers; Mozilla Open Source Support (MOSS); Patrick Figel -
+Greenpeace CEE; Pradeep Nayak - Circle Interactive; Rich Lott - Artful
+Robot; Seamus Lee - CiviCRM and JMA Consulting; Sean Colsen - Left Join
+Labs; Shitij Gugnai - Compucorp; Tim Otten - CiviCRM
diff --git a/civicrm/sql/civicrm.mysql b/civicrm/sql/civicrm.mysql
index f8eada46b4d5f925e5fdc4a84a20fcc0f5c2f2bd..b181200c6874be1e3bcf887448d09ac1845fd0d0 100644
--- a/civicrm/sql/civicrm.mysql
+++ b/civicrm/sql/civicrm.mysql
@@ -4354,8 +4354,8 @@ CREATE TABLE `civicrm_price_field_value` (
 
      `id` int unsigned NOT NULL AUTO_INCREMENT  COMMENT 'Price Field Value',
      `price_field_id` int unsigned NOT NULL   COMMENT 'FK to civicrm_price_field',
-     `name` varchar(255) NOT NULL   COMMENT 'Price field option name',
-     `label` varchar(255) NOT NULL   COMMENT 'Price field option label',
+     `name` varchar(255)   DEFAULT NULL COMMENT 'Price field option name',
+     `label` varchar(255)   DEFAULT NULL COMMENT 'Price field option label',
      `description` text   DEFAULT NULL COMMENT 'Price field option description.',
      `help_pre` text   DEFAULT NULL COMMENT 'Price field option pre help text.',
      `help_post` text   DEFAULT NULL COMMENT 'Price field option post field help.',
diff --git a/civicrm/sql/civicrm_data.mysql b/civicrm/sql/civicrm_data.mysql
index 12bbf583f36145b26231ce9be72f9698a534f6c3..9a30bb9939c8a96223789103cd70029899a81cff 100644
--- a/civicrm/sql/civicrm_data.mysql
+++ b/civicrm/sql/civicrm_data.mysql
@@ -23897,4 +23897,4 @@ INSERT INTO `civicrm_report_instance`
     ( `domain_id`, `title`, `report_id`, `description`, `permission`, `form_values`)
 VALUES
     (  @domainID, 'Survey Details', 'survey/detail', 'Detailed report for canvassing, phone-banking, walk lists or other surveys.', 'access CiviReport', 'a:39:{s:6:"fields";a:2:{s:9:"sort_name";s:1:"1";s:6:"result";s:1:"1";}s:22:"assignee_contact_id_op";s:2:"eq";s:25:"assignee_contact_id_value";s:0:"";s:12:"sort_name_op";s:3:"has";s:15:"sort_name_value";s:0:"";s:17:"street_number_min";s:0:"";s:17:"street_number_max";s:0:"";s:16:"street_number_op";s:3:"lte";s:19:"street_number_value";s:0:"";s:14:"street_name_op";s:3:"has";s:17:"street_name_value";s:0:"";s:15:"postal_code_min";s:0:"";s:15:"postal_code_max";s:0:"";s:14:"postal_code_op";s:3:"lte";s:17:"postal_code_value";s:0:"";s:7:"city_op";s:3:"has";s:10:"city_value";s:0:"";s:20:"state_province_id_op";s:2:"in";s:23:"state_province_id_value";a:0:{}s:13:"country_id_op";s:2:"in";s:16:"country_id_value";a:0:{}s:12:"survey_id_op";s:2:"in";s:15:"survey_id_value";a:0:{}s:12:"status_id_op";s:2:"eq";s:15:"status_id_value";s:1:"1";s:11:"custom_1_op";s:2:"in";s:14:"custom_1_value";a:0:{}s:11:"custom_2_op";s:2:"in";s:14:"custom_2_value";a:0:{}s:17:"custom_3_relative";s:1:"0";s:13:"custom_3_from";s:0:"";s:11:"custom_3_to";s:0:"";s:11:"description";s:75:"Detailed report for canvassing, phone-banking, walk lists or other surveys.";s:13:"email_subject";s:0:"";s:8:"email_to";s:0:"";s:8:"email_cc";s:0:"";s:10:"permission";s:17:"access CiviReport";s:6:"groups";s:0:"";s:9:"domain_id";i:1;}');
-UPDATE civicrm_domain SET version = '5.28.0';
+UPDATE civicrm_domain SET version = '5.28.1';
diff --git a/civicrm/sql/civicrm_generated.mysql b/civicrm/sql/civicrm_generated.mysql
index e5eb00b717d05817ad3f867c4753e745150f18fd..bbf0de2711ac4f901e4990d750f7cd8abd5e0676 100644
--- a/civicrm/sql/civicrm_generated.mysql
+++ b/civicrm/sql/civicrm_generated.mysql
@@ -399,7 +399,7 @@ UNLOCK TABLES;
 
 LOCK TABLES `civicrm_domain` WRITE;
 /*!40000 ALTER TABLE `civicrm_domain` DISABLE KEYS */;
-INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES (1,'Default Domain Name',NULL,'5.28.0',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
+INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES (1,'Default Domain Name',NULL,'5.28.1',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
 /*!40000 ALTER TABLE `civicrm_domain` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/civicrm/templates/CRM/Activity/Selector/Selector.tpl b/civicrm/templates/CRM/Activity/Selector/Selector.tpl
index ffb235266b9b01db7122fd5960f86b4778fa30fb..4817467ee5cc16e28ad0168db7a4eb6b89fa0069 100644
--- a/civicrm/templates/CRM/Activity/Selector/Selector.tpl
+++ b/civicrm/templates/CRM/Activity/Selector/Selector.tpl
@@ -50,7 +50,7 @@
 
   {literal}
     <script type="text/javascript">
-      (function($) {
+      (function($, _) {
         var context = {/literal}"{$context}"{literal};
         CRM.$('table.contact-activity-selector-' + context).data({
           "ajax": {
@@ -67,11 +67,16 @@
           }
         });
         $(function($) {
+          $('table.contact-activity-selector-' + context).on('xhr.dt', function(e, settings, json, xhr) {
+            for (var i=0, ien=json.data.length; i<ien; i++) {
+              json.data[i].subject = _.escape(json.data[i].subject);
+            }
+          });
           $('.activity-search-options :input').change(function(){
-            CRM.$('table.contact-activity-selector-' + context).DataTable().draw();
+            $('table.contact-activity-selector-' + context).DataTable().draw();
           });
         });
-      })(CRM.$);
+      })(CRM.$, CRM._);
     </script>
   {/literal}
   <style type="text/css">
diff --git a/civicrm/templates/CRM/Admin/Page/CKEditorConfig.tpl b/civicrm/templates/CRM/Admin/Form/CKEditorConfig.tpl
similarity index 82%
rename from civicrm/templates/CRM/Admin/Page/CKEditorConfig.tpl
rename to civicrm/templates/CRM/Admin/Form/CKEditorConfig.tpl
index 14af566be27a39197e590abce48a285869c94c5e..ae51de9d1815e995e00fa3657cd572b547b4f2a1 100644
--- a/civicrm/templates/CRM/Admin/Page/CKEditorConfig.tpl
+++ b/civicrm/templates/CRM/Admin/Form/CKEditorConfig.tpl
@@ -44,10 +44,6 @@
     border-bottom: 0 none;
     padding: 3px 10px 1px !important;
   }
-  .crm-config-option-row span.crm-error:after {
-    font-family: FontAwesome;
-    content: " \f071 Invalid JSON"
-  }
 {/literal}</style>
 {* Force the custom config file to reload by appending a new query string *}
 <script type="text/javascript">
@@ -64,7 +60,7 @@
     {/foreach}
   </ul>
 </div>
-<form method="post" action="{crmURL}" id="toolbarModifierForm">
+<div id="toolbarModifierForm">
   <fieldset>
     <div class="crm-block crm-form-block">
       <label for="skin">{ts}Skin{/ts}</label>
@@ -89,7 +85,6 @@
       </div>
     </div>
 
-
     <div class="crm-block crm-form-block">
       <fieldset>
         <legend>{ts}Advanced Options{/ts}</legend>
@@ -98,17 +93,9 @@
       </fieldset>
     </div>
 
-    <div class="crm-submit-buttons">
-      <span class="crm-button crm-i-button">
-        <i class="crm-i fa-wrench" aria-hidden="true"></i> <input type="submit" value="{ts}Save{/ts}" name="save" class="crm-form-submit" accesskey="S"/>
-      </span>
-      <span class="crm-button crm-i-button">
-        <i class="crm-i fa-times" aria-hidden="true"></i> <input type="submit" value="{ts}Revert to Default{/ts}" name="revert" class="crm-form-submit" onclick="return confirm('{$revertConfirm}');"/>
-      </span>
-    </div>
-    <input type="hidden" value="{$preset}" name="preset" />
+    <div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="bottom"}</div>
   </fieldset>
-</form>
+</div>
 <script type="text/template" id="config-row-tpl">
   <div class="crm-config-option-row">
     <input class="huge crm-config-option-name" placeholder="{ts}Option{/ts}"/>
diff --git a/civicrm/templates/CRM/Contact/Form/Search/Intro.tpl b/civicrm/templates/CRM/Contact/Form/Search/Intro.tpl
index d5d791c3b807d738b57fe467380e43ffa4c34b00..adc70018d03a78153a74ec7d66eb4b4cc943de96 100644
--- a/civicrm/templates/CRM/Contact/Form/Search/Intro.tpl
+++ b/civicrm/templates/CRM/Contact/Form/Search/Intro.tpl
@@ -10,26 +10,28 @@
 {* $context indicates where we are searching, values = "search,advanced,smog,amtg" *}
 {* smog = 'show members of group'; amtg = 'add members to group' *}
 {if $context EQ 'smog'}
-    {* Provide link to modify smart group search criteria if we are viewing a smart group (ssID = saved search ID) *}
+  {* Provide link to modify smart group search criteria if we are viewing a smart group (ssID = saved search ID) *}
+  {if $permissionEditSmartGroup}
     {if !empty($ssID)}
-        {if $ssMappingID}
-            {capture assign=editSmartGroupURL}{crmURL p="civicrm/contact/search/builder" q="reset=1&ssID=`$ssID`"}{/capture}
-        {elseif $savedSearch.search_custom_id}
-            {capture assign=editSmartGroupURL}{crmURL p="civicrm/contact/search/custom" q="reset=1&ssID=`$ssID`"}{/capture}
-        {else}
-            {capture assign=editSmartGroupURL}{crmURL p="civicrm/contact/search/advanced" q="reset=1&ssID=`$ssID`"}{/capture}
-        {/if}
-        <div class="crm-submit-buttons">
-            <a href="{$editSmartGroupURL}" class="button no-popup"><span><i class="crm-i fa-pencil" aria-hidden="true"></i> {ts 1=$group.title}Edit Smart Group Search Criteria for %1{/ts}</span></a>
-            {help id="id-edit-smartGroup"}
-        </div>
+      {if $ssMappingID}
+        {capture assign=editSmartGroupURL}{crmURL p="civicrm/contact/search/builder" q="reset=1&ssID=`$ssID`"}{/capture}
+      {elseif $savedSearch.search_custom_id}
+        {capture assign=editSmartGroupURL}{crmURL p="civicrm/contact/search/custom" q="reset=1&ssID=`$ssID`"}{/capture}
+      {else}
+        {capture assign=editSmartGroupURL}{crmURL p="civicrm/contact/search/advanced" q="reset=1&ssID=`$ssID`"}{/capture}
+      {/if}
+      <div class="crm-submit-buttons">
+        <a href="{$editSmartGroupURL}" class="button no-popup"><span><i class="crm-i fa-pencil" aria-hidden="true"></i> {ts 1=$group.title}Edit Smart Group Search Criteria for %1{/ts}</span></a>
+        {help id="id-edit-smartGroup"}
+      </div>
     {/if}
+  {/if}
 
-    {if $permissionedForGroup}
-        {capture assign=addMembersURL}{crmURL q="context=amtg&amtgID=`$group.id`&reset=1"}{/capture}
-        <div class="crm-submit-buttons">
-            <a href="{$addMembersURL}" class="button no-popup"><span><i class="crm-i fa-user-plus" aria-hidden="true"></i> {ts 1=$group.title}Add Contacts to %1{/ts}</span></a>
-            {if $ssID}{help id="id-add-to-smartGroup"}{/if}
-        </div>
-    {/if}
+  {if $permissionedForGroup}
+    {capture assign=addMembersURL}{crmURL q="context=amtg&amtgID=`$group.id`&reset=1"}{/capture}
+    <div class="crm-submit-buttons">
+      <a href="{$addMembersURL}" class="button no-popup"><span><i class="crm-i fa-user-plus" aria-hidden="true"></i> {ts 1=$group.title}Add Contacts to %1{/ts}</span></a>
+      {if $ssID}{help id="id-add-to-smartGroup"}{/if}
+    </div>
+  {/if}
 {/if}
diff --git a/civicrm/templates/CRM/Contribute/Form/Contribution/PremiumBlock.tpl b/civicrm/templates/CRM/Contribute/Form/Contribution/PremiumBlock.tpl
index db7e56220c678ebe73e392f5537e995dc3e4db36..6ca766cc26b31a2dbd8d4009d9d201ecc68647fd 100644
--- a/civicrm/templates/CRM/Contribute/Form/Contribution/PremiumBlock.tpl
+++ b/civicrm/templates/CRM/Contribute/Form/Contribution/PremiumBlock.tpl
@@ -332,8 +332,6 @@
           $('#selectProduct').rules('add', 'premiums');
         });
 
-        // need to use jquery validate's ignore option, so that it will not ignore hidden fields
-        CRM.validate.params['ignore'] = '.ignore';
       });
     </script>
     {/literal}
diff --git a/civicrm/templates/CRM/Core/BillingBlock.tpl b/civicrm/templates/CRM/Core/BillingBlock.tpl
index 10a830c1267b43a6c4265a1d74907bd71abb5eb9..f8bb2088f5064eb060a201e25cafe51ce1317bf6 100644
--- a/civicrm/templates/CRM/Core/BillingBlock.tpl
+++ b/civicrm/templates/CRM/Core/BillingBlock.tpl
@@ -20,7 +20,9 @@
         {foreach from=$paymentFields item=paymentField}
           {assign var='name' value=$form.$paymentField.name}
           <div class="crm-section {$form.$paymentField.name}-section">
-            <div class="label">{$form.$paymentField.label}</div>
+            <div class="label">{$form.$paymentField.label}
+              {if $requiredPaymentFields.$name}<span class="crm-marker" title="{ts}This field is required.{/ts}">*</span>{/if}
+            </div>
             <div class="content">
                 {$form.$paymentField.html}
               {if $paymentFieldsMetadata.$name.description}
@@ -49,7 +51,9 @@
         {foreach from=$billingDetailsFields item=billingField}
           {assign var='name' value=$form.$billingField.name}
           <div class="crm-section {$form.$billingField.name}-section">
-            <div class="label">{$form.$billingField.label}</div>
+            <div class="label">{$form.$billingField.label}
+              {if $requiredPaymentFields.$name}<span class="crm-marker" title="{ts}This field is required.{/ts}">*</span>{/if}
+            </div>
             {if $form.$billingField.type == 'text'}
               <div class="content">{$form.$billingField.html}</div>
             {else}
diff --git a/civicrm/templates/CRM/Event/Page/EventInfo.tpl b/civicrm/templates/CRM/Event/Page/EventInfo.tpl
index 38a9beaba2c67a1186a7924e614cf4c2e065f111..4b858973e4d9e609b379e664a5cfec1332269451 100644
--- a/civicrm/templates/CRM/Event/Page/EventInfo.tpl
+++ b/civicrm/templates/CRM/Event/Page/EventInfo.tpl
@@ -89,12 +89,12 @@
 
   {if $event.summary}
       <div class="crm-section event_summary-section">
-        {$event.summary}
+        {$event.summary|purify}
       </div>
   {/if}
   {if $event.description}
       <div class="crm-section event_description-section summary">
-          {$event.description}
+          {$event.description|purify}
       </div>
   {/if}
   <div class="clear"></div>
diff --git a/civicrm/templates/CRM/Event/Page/List.tpl b/civicrm/templates/CRM/Event/Page/List.tpl
index e5f5fa182f19c98b3c0ea81d370162e0ef5bc0aa..4cbf20b541d3734812f51880f39a0e87fb6880bd 100644
--- a/civicrm/templates/CRM/Event/Page/List.tpl
+++ b/civicrm/templates/CRM/Event/Page/List.tpl
@@ -30,7 +30,7 @@
     {foreach from=$events key=uid item=event}
       <tr class="{cycle values="odd-row,even-row"} {$row.class}">
         <td><a href="{crmURL p='civicrm/event/info' q="reset=1&id=`$event.event_id`"}" title="{ts}read more{/ts}"><strong>{$event.title}</strong></a></td>
-        <td>{if $event.summary}{$event.summary} (<a href="{crmURL p='civicrm/event/info' q="reset=1&id=`$event.event_id`"}" title="{ts}details...{/ts}">{ts}read more{/ts}...</a>){else}&nbsp;{/if}</td>
+        <td>{if $event.summary}{$event.summary|purify} (<a href="{crmURL p='civicrm/event/info' q="reset=1&id=`$event.event_id`"}" title="{ts}details...{/ts}">{ts}read more{/ts}...</a>){else}&nbsp;{/if}</td>
         <td class="nowrap" data-order="{$event.start_date|crmDate:'%Y-%m-%d'}">
           {if $event.start_date}{$event.start_date|crmDate}{if $event.end_date}<br /><em>{ts}through{/ts}</em><br />{strip}
             {* Only show end time if end date = start date *}
diff --git a/civicrm/templates/CRM/UF/Page/Group.tpl b/civicrm/templates/CRM/UF/Page/Group.tpl
index 1eb5ee8ef0a9a609f65ad607f1a9578e417017de..eb25d0e9b73f10cb2ce39299bcd8451d04a11729 100644
--- a/civicrm/templates/CRM/UF/Page/Group.tpl
+++ b/civicrm/templates/CRM/UF/Page/Group.tpl
@@ -77,7 +77,7 @@
                     <a href="{crmURL p='civicrm/contact/view' q="reset=1&cid=`$row.created_id`"}">{ts}{$row.created_by}{/ts}</a>
                   {/if}
                 </td>
-                <td class="crmf-description crm-editable" data-type="textarea">{$row.description}</td>
+                <td class="crmf-description crm-editable" data-type="textarea">{$row.description|escape}</td>
                 <td>{$row.group_type}</td>
                 <td>{$row.id}</td>
                 <td>{$row.module}</td>
@@ -122,7 +122,7 @@
                     <a href="{crmURL p='civicrm/contact/view' q="reset=1&cid=`$row.created_id`"}">{ts}{$row.created_by}{/ts}</a>
                   {/if}
                 </td>
-                <td>{$row.description}</td>
+                <td>{$row.description|escape}</td>
                 <td>{$row.group_type}</td>
                 <td>{$row.id}</td>
                 <td>{$row.module}</td>
diff --git a/civicrm/vendor/autoload.php b/civicrm/vendor/autoload.php
index a81209dbdf9ee5b5403c93ba21063e31c6f8f2b2..fd2598911f621ac26cd365b776ca7777e46d27fa 100644
--- a/civicrm/vendor/autoload.php
+++ b/civicrm/vendor/autoload.php
@@ -4,4 +4,4 @@
 
 require_once __DIR__ . '/composer/autoload_real.php';
 
-return ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1::getLoader();
+return ComposerAutoloaderInitcab70146bb388b68615bda3f614d51ef::getLoader();
diff --git a/civicrm/vendor/composer/autoload_real.php b/civicrm/vendor/composer/autoload_real.php
index b7d11681f559b4d1ad87451f7e126f47d3442610..db59d6b9be673624c32b9a2ced6260ad6983264b 100644
--- a/civicrm/vendor/composer/autoload_real.php
+++ b/civicrm/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
 
 // autoload_real.php @generated by Composer
 
-class ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1
+class ComposerAutoloaderInitcab70146bb388b68615bda3f614d51ef
 {
     private static $loader;
 
@@ -19,9 +19,9 @@ class ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1
             return self::$loader;
         }
 
-        spl_autoload_register(array('ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInitcab70146bb388b68615bda3f614d51ef', 'loadClassLoader'), true, true);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader();
-        spl_autoload_unregister(array('ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInitcab70146bb388b68615bda3f614d51ef', 'loadClassLoader'));
 
         $includePaths = require __DIR__ . '/include_paths.php';
         $includePaths[] = get_include_path();
@@ -31,7 +31,7 @@ class ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1
         if ($useStaticLoader) {
             require_once __DIR__ . '/autoload_static.php';
 
-            call_user_func(\Composer\Autoload\ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::getInitializer($loader));
+            call_user_func(\Composer\Autoload\ComposerStaticInitcab70146bb388b68615bda3f614d51ef::getInitializer($loader));
         } else {
             $map = require __DIR__ . '/autoload_namespaces.php';
             foreach ($map as $namespace => $path) {
@@ -52,19 +52,19 @@ class ComposerAutoloaderInit295933fd472bb10cd7a2a298afef50f1
         $loader->register(true);
 
         if ($useStaticLoader) {
-            $includeFiles = Composer\Autoload\ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::$files;
+            $includeFiles = Composer\Autoload\ComposerStaticInitcab70146bb388b68615bda3f614d51ef::$files;
         } else {
             $includeFiles = require __DIR__ . '/autoload_files.php';
         }
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire295933fd472bb10cd7a2a298afef50f1($fileIdentifier, $file);
+            composerRequirecab70146bb388b68615bda3f614d51ef($fileIdentifier, $file);
         }
 
         return $loader;
     }
 }
 
-function composerRequire295933fd472bb10cd7a2a298afef50f1($fileIdentifier, $file)
+function composerRequirecab70146bb388b68615bda3f614d51ef($fileIdentifier, $file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         require $file;
diff --git a/civicrm/vendor/composer/autoload_static.php b/civicrm/vendor/composer/autoload_static.php
index ea0ff2944cacb39dcd523051b7a8477796b73491..98f3adb753cf2ebfd171f2793416a0931d82b46f 100644
--- a/civicrm/vendor/composer/autoload_static.php
+++ b/civicrm/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@
 
 namespace Composer\Autoload;
 
-class ComposerStaticInit295933fd472bb10cd7a2a298afef50f1
+class ComposerStaticInitcab70146bb388b68615bda3f614d51ef
 {
     public static $files = array (
         '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
@@ -530,11 +530,11 @@ class ComposerStaticInit295933fd472bb10cd7a2a298afef50f1
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::$prefixDirsPsr4;
-            $loader->prefixesPsr0 = ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::$prefixesPsr0;
-            $loader->fallbackDirsPsr0 = ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::$fallbackDirsPsr0;
-            $loader->classMap = ComposerStaticInit295933fd472bb10cd7a2a298afef50f1::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInitcab70146bb388b68615bda3f614d51ef::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitcab70146bb388b68615bda3f614d51ef::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInitcab70146bb388b68615bda3f614d51ef::$prefixesPsr0;
+            $loader->fallbackDirsPsr0 = ComposerStaticInitcab70146bb388b68615bda3f614d51ef::$fallbackDirsPsr0;
+            $loader->classMap = ComposerStaticInitcab70146bb388b68615bda3f614d51ef::$classMap;
 
         }, null, ClassLoader::class);
     }
diff --git a/civicrm/xml/schema/Price/PriceFieldValue.xml b/civicrm/xml/schema/Price/PriceFieldValue.xml
index 74c649b916729353fbeecdde983ae791901cbbc0..2290d96282893aeaad029d8aa2b3cd672204a57e 100644
--- a/civicrm/xml/schema/Price/PriceFieldValue.xml
+++ b/civicrm/xml/schema/Price/PriceFieldValue.xml
@@ -37,11 +37,11 @@
     <title>Name</title>
     <length>255</length>
     <comment>Price field option name</comment>
-    <required>true</required>
     <html>
       <type>Text</type>
     </html>
     <add>3.3</add>
+    <default>NULL</default>
   </field>
   <field>
     <name>label</name>
@@ -50,11 +50,11 @@
     <length>255</length>
     <localizable>true</localizable>
     <comment>Price field option label</comment>
-    <required>true</required>
     <html>
       <type>Text</type>
     </html>
     <add>3.3</add>
+    <default>NULL</default>
   </field>
   <field>
     <name>description</name>
diff --git a/civicrm/xml/version.xml b/civicrm/xml/version.xml
index a33634d10543e2a863f800b4c8d827989fd7ba9e..846f4a4abe5626378381eb4aa815f5fdc82f04d3 100644
--- a/civicrm/xml/version.xml
+++ b/civicrm/xml/version.xml
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="iso-8859-1" ?>
 <version>
-  <version_no>5.28.0</version_no>
+  <version_no>5.28.1</version_no>
 </version>