HTML Form:输入类型隐藏值可见可变-记录处理状态

HTML Form: Input type hidden value is visible and changable - Recording Processed State

我正在开发一个需要 post 秘密变量的应用程序。我写了这段代码。

<form target="_blank" action="/validate/lista.php" method="POST">
            <input type="hidden" name="evento" value="<?php echo $pname ?>" />
            <button class="btn btn-block btn-md btn-outline-success">Lista</button>
</form>

我的问题是,如果用户使用 chrome 或其他方式检查元素,他可以看到值并在 POST 之前更改它。 我可以使用 SESSION,但每个用户都有不同的会话 ID,这样我就需要 POST 会话 ID(因为它们是单独的应用程序),我认为这是不安全的。或者可以吗?

我该如何防止这种情况发生?我是编程新手...

谢谢

安全地维护 HTML 表单状态('Conversation' 跟踪)

在客户端和服务器处理时跟踪 HTML 表单的 'state'。

典型的'conversation'是:

  1. 向客户端发送新表单,通常针对必须登录的特定用户。
  2. 客户端输入数据并returns它。
  3. 已通过验证,可以再次发出。
  4. 数据更改已应用。
  5. 通知客户结果。

听起来很简单。 las,我们需要在 'conversation' 期间跟踪表单的 'state'。

我们需要在隐藏字段中记录状态。这可以让我们接触到各种 'failure modes'。

此答案是可靠跟踪 'conversations' 的一种方法。

包括 'malicious' 的人。它发生了。 ;-/

这是一个数据更改表单,所以我们不希望它应用到错误的人身上。

有各种要求:

懂事的人:

  1. 防止表单被处理两次
  2. 如果表格太旧,请用户确认数据

恶意的:

  1. 将表单更改为来自其他用户
  2. 使用表格的旧副本
  3. 更改其他隐藏数据以破坏用户数据

现在,我们无法阻止客户端更改隐藏数据,或存储它以供稍后重播。等等

怎么办?

  1. 我们需要确保如果它被更改,那么我们可以检测到它被篡改并告知用户。我们什么都不做。
  2. 如果他们向我们发送了旧的有效副本,那么我们也可以检测到。

有没有简单的方法可以做到这一点?哦是的! :)

答案:

  1. 给每个表单一个唯一的 ID:可以很容易地确定我们是否已经看过它。
  2. 给每个表单一个时间戳,表示它首次创建的时间。

然后我们可以决定我们允许使用它的最大年龄。 如果它太旧,那么我们只需将输入的数据复制到新表单并要求用户确认。查看验证码:)

当我们处理表单时,我们存储表单 ID。

处理表格之前的第一个检查是看我们是否已经处理过它

识别'tampering'?

我们用AES加密! :) 只有服务器需要知道密码,所以没有客户端问题。

如果它被更改,那么解密将失败,我们只是向用户发出一个新的表单,其中包含输入的数据。 :)

代码很多吗?并不真地。它使表单处理安全。

一个优点是内置了对 CSRF 攻击的保护,因此不需要单独的代码。

程序代码(FormState Class)

<?php
/**
* every 'data edit' form has one of these - without exeception.
*
* This ensures that the form I sent out came from me. 
* 
* It has: 
*    1) A unique @id
*    2) A date time stamp and a lifetime
* 
*  Can be automatically generated and checked. 
*/

class FormState {

    const MAX_FORM_AGE = 600; // seconds 

    const ENC_PASSWORD = '327136823981d9e57652bba2acfdb1f2';   
    const ENC_IV       = 'f9928260b550dbb2eecb6e10fcf630ba';   

    protected $state = array();

    public function __construct($prevState = '')
    {
        if (!empty($prevState)) {
            $this->reloadState($prevState); // will not be valid if fails
            return;
        }

        $this->setNewForm();
    }

    /**
     * Generate a new unique id and timestanp
     *
     * @param $name - optional name for the form
     */
    public function setNewForm($name = '')
    {
        $this->state = array();
        $this->state['formid'] = sha1(uniqid(true)); // each form has a unique id 
        $this->state['when'] = time();

        if (!empty($name)) {
            $this->setAttribute('name', $name);
        }
    }

    /**
     * retrieve attribute value
     *
     * @param $name     attribute name to use
     * @param $default  value to return if attribute does not exist
     * 
     * @return string / number
     */
    public function getAttribute($name, $default = null) 
    {
            if (isset($this->state[$name])) {
                   return $this->state[$name];
            } else {
                   return $default;
            }   
    }

    /**
     * store attribute value
     *
     * @param $name     attribute name to use
     * @param $value    value to save
     */
    public function setAttribute($name, $value) 
    {
            $this->state[$name] = $value;
    }

    /**
     * get the array
     */
    public function getAllAttributes()
    {
        return $this->state;
    } 

    /**
     * the unique form id
     *  
     * @return hex string
     */
    public function getFormId()
    {
        return $this->getAttribute('formid');    
    }

    /**
     * Age of the form in seconds
     * @return int seconds
     */
    public function getAge()
    {
        if ($this->isValid()) {
            return time() - $this->state['when'];
        }
        return 0;
    }

    /**
     * check the age of the form
     * 
     *@param $ageSeconds is age older than the supplied age 
     */
    public function isOutOfDate($ageSeconds = self::MAX_FORM_AGE)
    {
        return $this->getAge() >= $ageSeconds;
    }

    /**
     * was a valid string passed when restoring it
     * @return boolean
     */
    public function isValid()
    { 
        return is_array($this->state) && !empty($this->state);
    }

    /** -----------------------------------------------------------------------
     * Encode as string - these are encrypted to ensure they are not tampered with  
     */

    public function asString()
    {        
        $serialized = serialize($this->state);
        $encrypted = $this->encrypt_decrypt('encrypt', $serialized);

        $result = base64_encode($encrypted);
        return $result;
    }

    /**
     * Restore the saved attributes - it must be a valid string 
     *
     * @Param $prevState
     * @return array Attributes
     */
    public function fromString($prevState)
    {
        $encrypted = @base64_decode($prevState);
        if ($encrypted === false) {
           return false; 
        }

        $serialized = $this->encrypt_decrypt('decrypt', $encrypted);
        if ($serialized === false) {
           return false; 
        }

        $object = @unserialize($serialized);
        if ($object === false) {
           return false; 
        }

        if (!is_array($object)) {
            throw new \Exception(__METHOD__ .' failed to return object: '. $object, 500);
        }
        return $object; 
    }

    public function __toString()
    {
        return $this->asString();
    }

    /**
     * Restore the previous state of the form 
     *    will not be valid if not a valid string
     *
     * @param  $prevState  an encoded serialized array
     * @return bool isValid or not
     */
    public function reloadState($prevState)
    {
        $this->state = array();

        $state = $this->fromString($prevState);
        if ($state !== false) {
            $this->state = $state;
        }

        return $this->isValid();
    }

    /**
     * simple method to encrypt or decrypt a plain text string
     * initialization vector(IV) has to be the same when encrypting and decrypting
     * 
     * @param string $action: can be 'encrypt' or 'decrypt'
     * @param string $string: string to encrypt or decrypt
     *
     * @return string
     */
    public function encrypt_decrypt($action, $string) 
    {
        $output = false;

        $encrypt_method = "AES-256-CBC";
        $secret_key = self::ENC_PASSWORD;


        // iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
        $secret_iv_len = openssl_cipher_iv_length($encrypt_method);
        $secret_iv = substr(self::ENC_IV, 0, $secret_iv_len);

        if ( $action == 'encrypt' ) {
            $output = openssl_encrypt($string, $encrypt_method, $secret_key, OPENSSL_RAW_DATA, $secret_iv);

        } else if( $action == 'decrypt' ) {
            $output = openssl_decrypt($string, $encrypt_method, $secret_key, OPENSSL_RAW_DATA, $secret_iv);
        }

        if ($output === false) {
            // throw new \Exception($action .' failed: '. $string, 500);
        }

        return $output;
    }
}

示例代码

Full Example Application Source Code (Q49924789)

Website Using the supplied Source Code

FormState source code

我们有现成的表格吗?

$isExistingForm = !empty($_POST['formState']);
$selectedAction = 'start-NewForm'; // default action

  if  ($isExistingForm) { // restore state  
    $selectedAction = $_POST['selectedAction'];      
    $formState = new \FormState($_POST['formState']); // it may be invalid
    if (!$formState->isValid() && $selectedAction !== 'start-NewForm') {
        $selectedAction = "formState-isWrong"; // force user to start a new form
    }

} else {
    $_POST = array();  // yes, $_POST is just another PHP array 
    $formState = new \FormState();    
}

开始新表格

    $formState = new \FormState();
    $_POST = array(); 
    $displayMsg = "New formstate created. FormId: ". $formState->getFormId();

在FormState中存储UserId(数据库Id)

        $formState->setAttribute('userId' $userId);

查看表格是不是太旧了?

  $secsToBeOutOfDate = 3;

  if ($formState->isOutOfDate($secsToBeOutOfDate)) {
    $errorMsg = 'Out Of Date Age: '. $secsToBeOutOfDate .'secs'
                .', ActualAge: '. $formState->getAge();
  }             

从表单隐藏字段重新加载状态。

  $formState = new \FormState('this is rubbish!!');
  $errorMsg = "formState: isValid(): ". ($formState->isValid() ? 'True' : 'False');        

检查表格是否已经处理。

  if (isset($_SESSION['processedForms'][$formState->getFormId()])) {
      $errorMsg = 'This form has already been processed. (' . $formState->getFormId() .')';
      break;
  }

  $_SESSION['processedForms'][$formState->getFormId()]  = true;
  $displayMsg = "Form processed and added to list.";