Просмотр исходного кода

#5529 Protect confidential information: view form secrets

Aleksandr Trofimov 2 месяцев назад
Родитель
Сommit
20b5668cf4

+ 3 - 0
inc/classes/BxDolForm.php

@@ -175,6 +175,9 @@ bx_import('BxDolPrivacy');
  * - attrs
  * - attrs_tr
  * - attrs_wrapper
+ * - secret - if true, in view mode the value is shown masked (for example `abcd********xyz1`) with an eye icon on the right.
+ *            Clicking the icon reloads the current page block with that field revealed. Reveal is POSTed with a form CSRF token, so a GET param alone is not enough. The full value is not included in the initial HTML.
+ *            Example: `$this->aInputs['password']['secret'] = true;`
  *
  * We will not describe above list of parameters in every type, since they work the same way for all types.
  *

+ 41 - 4
inc/js/functions.js

@@ -159,7 +159,7 @@ function loadDynamicBlockAutoSort (e, sort, sAdditionalUrlParams) {
  * @param e - element inside the block
  * @return true on success, or false on error - particularly, if block isn't found
  */
-function loadDynamicBlockAuto(e, sUrl) {
+function loadDynamicBlockAuto(e, sUrl, oPostData) {
     var oContainer = $(e).parents('.bx-page-block-container:first:not(.no-dynamic)');
     var sId = oContainer.attr('id');
 
@@ -187,17 +187,54 @@ function loadDynamicBlockAuto(e, sUrl) {
     	sUrl = bx_append_url_params(sUrl, oParams);
     }
 
-    loadDynamicBlock(parseInt(aMatches[1]), sUrl);
+    loadDynamicBlock(parseInt(aMatches[1]), sUrl, oPostData);
     return true;
 }
 
-function loadDynamicBlock(iBlockID, sUrl) {
+/**
+ * Reload the current page block to show or hide a form secret field.
+ * Reveal is POSTed with a CSRF token so it cannot be triggered by a GET param alone.
+ * When the form is not inside a page block, the whole page is reloaded via POST.
+ */
+function bx_form_secret_toggle(e) {
+    var oToggle = $(e);
+    var bHide = oToggle.closest('.bx-form-secret').hasClass('bx-form-secret-shown');
+    var sField = oToggle.attr('bx_form_secret_field');
+    var sCsrfToken = oToggle.attr('bx_form_secret_csrf');
+
+    var sUrl = location.href;
+    try {
+        var oUrl = new URL(sUrl);
+        oUrl.searchParams.delete('form_secret_reveal');
+        oUrl.searchParams.delete('csrf_token');
+        sUrl = oUrl.toString();
+    } catch (oErr) {
+        sUrl = sUrl.replace(/([?&])form_secret_reveal=[^&]*/g, '$1').replace(/([?&])csrf_token=[^&]*/g, '$1').replace(/[?&]$/, '');
+    }
+
+    if (bHide) {
+        if (!loadDynamicBlockAuto(e, sUrl))
+            location.href = sUrl;
+        return false;
+    }
+
+    var oData = {form_secret_reveal: sField};
+    if (sCsrfToken)
+        oData.csrf_token = sCsrfToken;
+
+    return loadDynamicBlockAuto(e, sUrl, oData);
+}
+
+function loadDynamicBlock(iBlockID, sUrl, oPostData) {
     
     var oCallback = null;
     if($('#bx-page-block-' + iBlockID + ' .bx-base-unit-showcase-wrapper').length){
         oCallback = bx_showcase_view_init;
     }
-    getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID), oCallback);
+    if (typeof oPostData != 'undefined' && oPostData)
+        getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID), oCallback, 'post', false, oPostData);
+    else
+        getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID), oCallback);
     return true;
 }
 

+ 21 - 0
modules/boonex/artificer/data/template/system/css/forms.css

@@ -433,3 +433,24 @@ import url(../../../../../../../template/css/forms.css);
     border-bottom:none !important;
 }
 
+.bx-form-secret {
+    display: inline-flex;
+    align-items: center;
+    justify-content: flex-start;
+    gap: 0.5rem;
+    max-width: 100%;
+    min-width: 0;
+}
+.bx-form-secret-value {
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-family: monospace;
+}
+.bx-form-secret-toggle {
+    flex-shrink: 0;
+    line-height: 1;
+    cursor: pointer;
+}
+

+ 2 - 0
modules/boonex/english/data/langs/system/en.xml

@@ -3238,6 +3238,8 @@ If it is not enabled then please consider implementing this optimization, since
 	<string name="_sys_form_review_input_caption_system_id"><![CDATA[Object ID]]></string>
 	<string name="_sys_form_review_input_caption_system_sys"><![CDATA[Object Type]]></string>
 	<string name="_sys_form_review_input_submit"><![CDATA[Post]]></string>
+	<string name="_sys_form_secret_hide"><![CDATA[Hide]]></string>
+	<string name="_sys_form_secret_reveal"><![CDATA[Show]]></string>
 	<string name="_sys_form_unsubscribe"><![CDATA[Unsubscribe]]></string>
 	<string name="_sys_form_unsubscribe_input_caption_system_code"><![CDATA[Security Code]]></string>
 	<string name="_sys_form_unsubscribe_input_caption_system_do_submit"><![CDATA[Submit]]></string>

+ 2 - 0
modules/boonex/russian/data/langs/system/ru.xml

@@ -3212,6 +3212,8 @@
 	<string name="_sys_form_review_input_caption_system_id"><![CDATA[ID объекта]]></string>
 	<string name="_sys_form_review_input_caption_system_sys"><![CDATA[Тип объекта]]></string>
 	<string name="_sys_form_review_input_submit"><![CDATA[Отправить]]></string>
+	<string name="_sys_form_secret_hide"><![CDATA[Скрыть]]></string>
+	<string name="_sys_form_secret_reveal"><![CDATA[Показать]]></string>
 	<string name="_sys_form_unsubscribe"><![CDATA[Отписаться]]></string>
 	<string name="_sys_form_unsubscribe_input_caption_system_code"><![CDATA[Код безопасности]]></string>
 	<string name="_sys_form_unsubscribe_input_caption_system_do_submit"><![CDATA[Выполнить]]></string>

+ 21 - 0
template/css/forms.css

@@ -445,6 +445,27 @@
     font-weight:bold;
 }
 
+.bx-form-secret {
+    display: inline-flex;
+    align-items: center;
+    justify-content: flex-start;
+    gap: 0.5rem;
+    max-width: 100%;
+    min-width: 0;
+}
+.bx-form-secret-value {
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-family: monospace;
+}
+.bx-form-secret-toggle {
+    flex-shrink: 0;
+    line-height: 1;
+    cursor: pointer;
+}
+
 
 /****************************************
 *		Custom reCaptcha Theme

+ 4 - 0
template/form_view_secret.html

@@ -0,0 +1,4 @@
+<div class="bx-form-secret __class__">
+    <span class="bx-form-secret-value">__value__</span>
+    <a href="javascript:void(0);" class="bx-form-secret-toggle" title="__title__" bx_form_secret_field="__name__" bx_form_secret_csrf="__csrf__" onclick="return bx_form_secret_toggle(this);"><i class="sys-icon __icon__"></i></a>
+</div>

+ 66 - 0
template/scripts/BxBaseFormView.php

@@ -72,6 +72,11 @@ class BxBaseFormView extends BxDolForm
      */
     protected $_bViewMode = false;
 
+    /**
+     * Cached CSRF check for revealing a secret field in the current request.
+     */
+    protected $_bSecretRevealCsrfValid = null;
+
     /**
      * Show or not sections which have no fileds
      */
@@ -502,6 +507,11 @@ class BxBaseFormView extends BxDolForm
                 ];
             }
 
+            if ($this->_isSecretInput($aInput) && isset($aInput['value']) && $aInput['value'] !== '' && $aInput['value'] !== null && !is_array($aInput['value'])) {
+                if (!$this->isViewMode() || !$this->_isSecretRevealed($aInput['name']))
+                    $aInput['value'] = bx_gen_secret((string)$aInput['value']);
+            }
+
             /**
              * Everytime when [key => value] is used it should be converted to [key => ..., value => ...]
              */
@@ -918,6 +928,11 @@ BLAH;
         $sClass = !empty($aParams['class']) ? $aParams['class'] : '';
         $sClassValue = !empty($aParams['class_value']) ? $aParams['class_value'] : '';
 
+        if ($this->_isSecretInput($aInput)) {
+            $sValue = $this->_wrapSecretViewRowValue($aInput, $sValue, $this->_isSecretRevealed($aInput['name']));
+            $sClassValue .= ($sClassValue ? ' ' : '') . 'bx-form-row-view-value-secret';
+        }
+
         $aTmplVarsIcon = [];
         if(!empty($aInput['icon']))
             $aTmplVarsIcon = [
@@ -945,6 +960,11 @@ BLAH;
      */
     function genViewRowValue(&$aInput)
     {
+        if ($this->_isSecretInput($aInput) && !$this->_isSecretRevealed($aInput['name'])) {
+            $sRaw = $this->_getSecretRawValue($aInput);
+            return $sRaw !== '' ? bx_process_output(bx_gen_secret($sRaw)) : null;
+        }
+
         if (!empty($aInput['name'])) {
             $sCustomMethod = 'genCustomViewRowValue' . $this->_genMethodName($aInput['name']);
             if (method_exists($this, $sCustomMethod))
@@ -1048,6 +1068,52 @@ BLAH;
         return $s;
     }
 
+    protected function _isSecretInput($aInput)
+    {
+        return !empty($aInput['secret']) && !empty($aInput['name']);
+    }
+
+    protected function _isSecretRevealed($sName)
+    {
+        $mixed = bx_get('form_secret_reveal');
+        if ($mixed === false || $sName === '' || $sName === null)
+            return false;
+        if (is_array($mixed)) {
+            if (!in_array((string)$sName, array_map('strval', $mixed), true))
+                return false;
+        } else if ((string)$mixed !== (string)$sName) {
+            return false;
+        }
+
+        if ($this->_bSecretRevealCsrfValid === null) {
+            $mixedToken = bx_get('csrf_token');
+            $this->_bSecretRevealCsrfValid = BxDolForm::isCsrfTokenValid($mixedToken === false ? '' : $mixedToken);
+        }
+
+        return $this->_bSecretRevealCsrfValid;
+    }
+
+    protected function _getSecretRawValue(&$aInput)
+    {
+        if (!isset($aInput['value']) || $aInput['value'] === '' || $aInput['value'] === null || is_array($aInput['value']))
+            return '';
+        return (string)$aInput['value'];
+    }
+
+    protected function _wrapSecretViewRowValue(&$aInput, $sValue, $bRevealed = false)
+    {
+        $mixedToken = !$bRevealed ? BxDolForm::getCsrfToken() : false;
+
+        return $this->oTemplate->parseHtmlByName('form_view_secret.html', [
+            'class' => $bRevealed ? 'bx-form-secret-shown' : '',
+            'value' => $sValue,
+            'title' => bx_html_attribute(_t($bRevealed ? '_sys_form_secret_hide' : '_sys_form_secret_reveal')),
+            'name' => bx_html_attribute($aInput['name']),
+            'csrf' => $mixedToken ? bx_html_attribute($mixedToken) : '',
+            'icon' => $bRevealed ? 'eye-slash' : 'eye',
+        ]);
+    }
+
     protected function genCustomRowBirthday(&$aInput)
     {
         if(!$this->_bViewMode)