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'是:
- 向客户端发送新表单,通常针对必须登录的特定用户。
- 客户端输入数据并returns它。
- 已通过验证,可以再次发出。
- 数据更改已应用。
- 通知客户结果。
听起来很简单。 las,我们需要在 'conversation' 期间跟踪表单的 'state'。
我们需要在隐藏字段中记录状态。这可以让我们接触到各种 'failure modes'。
此答案是可靠跟踪 'conversations' 的一种方法。
包括 'malicious' 的人。它发生了。 ;-/
这是一个数据更改表单,所以我们不希望它应用到错误的人身上。
有各种要求:
懂事的人:
- 防止表单被处理两次
- 如果表格太旧,请用户确认数据
恶意的:
- 将表单更改为来自其他用户
- 使用表格的旧副本
- 更改其他隐藏数据以破坏用户数据
现在,我们无法阻止客户端更改隐藏数据,或存储它以供稍后重播。等等
怎么办?
- 我们需要确保如果它被更改,那么我们可以检测到它被篡改并告知用户。我们什么都不做。
- 如果他们向我们发送了旧的有效副本,那么我们也可以检测到。
有没有简单的方法可以做到这一点?哦是的! :)
答案:
- 给每个表单一个唯一的 ID:可以很容易地确定我们是否已经看过它。
- 给每个表单一个时间戳,表示它首次创建的时间。
然后我们可以决定我们允许使用它的最大年龄。
如果它太旧,那么我们只需将输入的数据复制到新表单并要求用户确认。查看验证码:)
当我们处理表单时,我们存储表单 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
我们有现成的表格吗?
$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.";
我正在开发一个需要 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'是:
- 向客户端发送新表单,通常针对必须登录的特定用户。
- 客户端输入数据并returns它。
- 已通过验证,可以再次发出。
- 数据更改已应用。
- 通知客户结果。
听起来很简单。 las,我们需要在 'conversation' 期间跟踪表单的 'state'。
我们需要在隐藏字段中记录状态。这可以让我们接触到各种 'failure modes'。
此答案是可靠跟踪 'conversations' 的一种方法。
包括 'malicious' 的人。它发生了。 ;-/
这是一个数据更改表单,所以我们不希望它应用到错误的人身上。
有各种要求:
懂事的人:
- 防止表单被处理两次
- 如果表格太旧,请用户确认数据
恶意的:
- 将表单更改为来自其他用户
- 使用表格的旧副本
- 更改其他隐藏数据以破坏用户数据
现在,我们无法阻止客户端更改隐藏数据,或存储它以供稍后重播。等等
怎么办?
- 我们需要确保如果它被更改,那么我们可以检测到它被篡改并告知用户。我们什么都不做。
- 如果他们向我们发送了旧的有效副本,那么我们也可以检测到。
有没有简单的方法可以做到这一点?哦是的! :)
答案:
- 给每个表单一个唯一的 ID:可以很容易地确定我们是否已经看过它。
- 给每个表单一个时间戳,表示它首次创建的时间。
然后我们可以决定我们允许使用它的最大年龄。 如果它太旧,那么我们只需将输入的数据复制到新表单并要求用户确认。查看验证码:)
当我们处理表单时,我们存储表单 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
我们有现成的表格吗?
$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.";