MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Form;
|
||||
|
||||
use MRBS\Errors\Errors;
|
||||
use MRBS\Exception;
|
||||
use function MRBS\generate_token;
|
||||
use function MRBS\get_form_var;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\session;
|
||||
|
||||
|
||||
class Form extends Element
|
||||
{
|
||||
public const METHOD_GET = 0;
|
||||
public const METHOD_POST = 1;
|
||||
private const TOKEN_NAME = 'csrf_token';
|
||||
|
||||
private static $token = null;
|
||||
private static $cookie_set = false;
|
||||
|
||||
|
||||
// Creates a new form, automatically adding a CSRF token and a MAX_FILE_SIZE value
|
||||
// as hidden inputs if the method is POST. The GET method should only be used for
|
||||
// navigating between pages and not for submitting form data which could change the
|
||||
// database contents or reveal data to which the user is not entitled.
|
||||
public function __construct(int $method=self::METHOD_GET)
|
||||
{
|
||||
parent::__construct('form');
|
||||
$this->setMethod($method);
|
||||
}
|
||||
|
||||
|
||||
// Sets the form method. If the method is POST then a CSRF token and a MAX_FILE_SIZE
|
||||
// value are also added as hidden inputs. If the method is GET then these hidden
|
||||
// inputs are removed because, in the case of the CSRF token we don't want it to
|
||||
// appear in the URL for security reasons, and in the case of MAX_FILE_SIZE it should
|
||||
// only be needed for POST requests.
|
||||
// TODO: if this method is called after the constructor then make sure the MAX_FILE_SIZE
|
||||
// TODO: hidden input is at the beginning, before any possible file input elements.
|
||||
private function setMethod(int $method) : Element
|
||||
{
|
||||
if ($method === self::METHOD_GET)
|
||||
{
|
||||
$this->removeHiddenInput('MAX_FILE_SIZE');
|
||||
$this->removeHiddenInput(self::TOKEN_NAME);
|
||||
}
|
||||
|
||||
elseif ($method === self::METHOD_POST)
|
||||
{
|
||||
// Add a MAX_FILE_SIZE hidden input for use by forms that have a file
|
||||
// upload input. This hidden input must come before the file input
|
||||
// element if it is to be used by PHP. Although at the time of writing
|
||||
// it is not used by any browsers, we can add some JavaScript to check
|
||||
// the file size when it is selected and thus save a failed upload attempt.
|
||||
$max_file_size = ini_get('upload_max_filesize');
|
||||
if ($max_file_size !== false)
|
||||
{
|
||||
$max_file_size = self::convertToBytes($max_file_size);
|
||||
$this->addHiddenInput('MAX_FILE_SIZE', $max_file_size, 'MAX_FILE_SIZE');
|
||||
}
|
||||
// Add a CSRF token
|
||||
$this->addCSRFToken();
|
||||
}
|
||||
|
||||
return parent::setAttribute('method', self::methodToString($method));
|
||||
}
|
||||
|
||||
|
||||
// Converts a method string (eg 'get' or 'GET') to a method constant (eg self::METHOD_GET)
|
||||
private static function methodToInt(string $string) : int
|
||||
{
|
||||
if (strcasecmp($string, 'get') === 0)
|
||||
{
|
||||
return self::METHOD_GET;
|
||||
}
|
||||
|
||||
if (strcasecmp($string, 'post') === 0)
|
||||
{
|
||||
return self::METHOD_POST;
|
||||
}
|
||||
|
||||
throw new Exception("Unsupported method $string");
|
||||
}
|
||||
|
||||
|
||||
// Converts a method constant (eg self::METHOD_GET) to a method string (eg 'get')
|
||||
private static function methodToString(int $int) : string
|
||||
{
|
||||
if ($int === self::METHOD_GET)
|
||||
{
|
||||
return 'get';
|
||||
}
|
||||
|
||||
if ($int === self::METHOD_POST)
|
||||
{
|
||||
return 'post';
|
||||
}
|
||||
|
||||
throw new Exception("Unsupported method constant $int");
|
||||
}
|
||||
|
||||
|
||||
// Sets a form attribute, taking special action in the case of the
|
||||
// method attribute to set/unset the CSRF token and MAX_FILE_SIZE
|
||||
// hidden inputs. Can cope with either string or integer method values.
|
||||
public function setAttribute(string $name, $value=true): Element
|
||||
{
|
||||
if (strcasecmp($name, 'method') === 0)
|
||||
{
|
||||
if (is_string($value))
|
||||
{
|
||||
$value = self::methodToInt($value);
|
||||
}
|
||||
if ($value === self::METHOD_POST)
|
||||
{
|
||||
$message = "Changing the form method after the form has been created may result " .
|
||||
"in the MAX_FILE_SIZE hidden input coming after a file input, thus " .
|
||||
"making it unusable by the server.";
|
||||
trigger_error($message, E_USER_WARNING);
|
||||
}
|
||||
return $this->setMethod($value);
|
||||
}
|
||||
|
||||
return parent::setAttribute($name, $value);
|
||||
}
|
||||
|
||||
|
||||
// Adds a hidden input to the form. Optionally give the element a key
|
||||
// so that it can be removed later using the same key.
|
||||
public function addHiddenInput(string $name, $value, ?string $key=null) : Form
|
||||
{
|
||||
$element = new ElementInputHidden($name, $value);
|
||||
$this->addElement($element, $key);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Adds an array of hidden inputs to the form
|
||||
public function addHiddenInputs(array $hidden_inputs) : Form
|
||||
{
|
||||
foreach ($hidden_inputs as $key => $value)
|
||||
{
|
||||
$this->addHiddenInput($key, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Removes a hidden input from the form.
|
||||
private function removeHiddenInput(string $key) : Form
|
||||
{
|
||||
$this->removeElement($key);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Returns the HTML for a hidden field containing a CSRF token
|
||||
public static function getTokenHTML() : string
|
||||
{
|
||||
$element = new ElementInputHidden();
|
||||
$element->setAttributes(array('name' => self::TOKEN_NAME,
|
||||
'value' => self::getToken()));
|
||||
return $element->toHTML();
|
||||
}
|
||||
|
||||
|
||||
// Checks the CSRF token against the stored value and dies with a fatal error
|
||||
// if they do not match. Note that:
|
||||
// (1) The CSRF token is always looked for in the POST data, never anywhere else.
|
||||
// GET requests should only be used for operations that do not modify data or
|
||||
// grant access.
|
||||
// (2) Forms should normally use a POST method.
|
||||
// (3) Actions should normally be taken by handler pages which are not designed to be
|
||||
// accessed directly by the user and are only expecting POST requests. These pages
|
||||
// will look for the CSRF token however they are requested. If they are requested via
|
||||
// GET then they will still look for the token in the POST data and so fail.
|
||||
// (4) There are some MRBS pages that can be accessed either via a URL with query string,
|
||||
// or via a POST request. These pages should not take any action, but as a matter of
|
||||
// good practice should check the token anyway if they have been requested by a POST.
|
||||
// To cater for these pages the $post_only parameter should be set to TRUE.
|
||||
public static function checkToken(bool $post_only=false) : void
|
||||
{
|
||||
global $server;
|
||||
|
||||
if ($post_only && ($server['REQUEST_METHOD'] != 'POST'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$token = get_form_var(self::TOKEN_NAME, 'string', null, INPUT_POST);
|
||||
$stored_token = self::getStoredToken();
|
||||
|
||||
if (!self::compareTokens($stored_token, $token))
|
||||
{
|
||||
if (isset($stored_token))
|
||||
{
|
||||
// Only report a possible CSRF attack if the stored token exists. If it doesn't
|
||||
// it's normally because the user session has expired in between the form being
|
||||
// displayed and submitted.
|
||||
trigger_error('Possible CSRF attack from IP address ' . $server['REMOTE_ADDR'], E_USER_NOTICE);
|
||||
}
|
||||
|
||||
if (method_exists(session(), 'logoffUser'))
|
||||
{
|
||||
session()->logoffUser();
|
||||
}
|
||||
|
||||
Errors::fatalError(get_vocab("session_expired"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// $max_unit can be set to 'seconds', 'minutes', 'hours', etc. and
|
||||
// can be used to specify the maximum unit to return.
|
||||
public static function getTimeUnitOptions($max_unit=null) : array
|
||||
{
|
||||
$options = array();
|
||||
$units = array('seconds', 'minutes', 'hours', 'days', 'weeks');
|
||||
|
||||
foreach ($units as $unit)
|
||||
{
|
||||
$options[$unit] = get_vocab($unit);
|
||||
if (isset($max_unit) && ($max_unit == $unit))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
|
||||
private function addCSRFToken() : Form
|
||||
{
|
||||
$this->addHiddenInput(self::TOKEN_NAME, self::getToken(), self::TOKEN_NAME);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
// Get a CSRF token
|
||||
public static function getToken() : string
|
||||
{
|
||||
$token_length = 32;
|
||||
|
||||
if (!isset(self::$token))
|
||||
{
|
||||
$stored_token = self::getStoredToken();
|
||||
// The test below should really be isset() rather than !empty(). However occasionally MRBS has the
|
||||
// value 0 stored in the session variable. It's not clear how or why this is happening. Until the
|
||||
// root cause is found we test for empty() and if the token is set but empty we generate a new token.
|
||||
// Update: it seems that when the token is 0, so are all the other session variables. So the problem
|
||||
// is probably not in the form code, but elsewhere.
|
||||
if (!empty($stored_token))
|
||||
{
|
||||
self::$token = $stored_token;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isset($stored_token))
|
||||
{
|
||||
// The token is set but empty
|
||||
$message = "Stored token is '$stored_token'. This should not be possible. " .
|
||||
"Generating a new token.";
|
||||
trigger_error($message,E_USER_WARNING);
|
||||
}
|
||||
self::$token = generate_token($token_length);
|
||||
self::storeToken(self::$token);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$token;
|
||||
}
|
||||
|
||||
|
||||
// Compare two tokens in a timing attack safe manner.
|
||||
// Returns true if they are equal, otherwise false.
|
||||
// Note: it is important to provide the user-supplied string as the
|
||||
// second parameter, rather than the first.
|
||||
private static function compareTokens($known_token, $user_token) : bool
|
||||
{
|
||||
if (is_null($known_token) || is_null($user_token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function_exists('hash_equals'))
|
||||
{
|
||||
return hash_equals($known_token, $user_token);
|
||||
}
|
||||
|
||||
// Could do fancier things here to give a timing attack safe comparison,
|
||||
// For example https://github.com/indigophp/hash-compat
|
||||
return ($known_token === $user_token);
|
||||
}
|
||||
|
||||
|
||||
private static function storeToken($token) : void
|
||||
{
|
||||
session()->set(self::TOKEN_NAME, $token);
|
||||
}
|
||||
|
||||
|
||||
private static function getStoredToken() : ?string
|
||||
{
|
||||
$result = session()->get(self::TOKEN_NAME);
|
||||
|
||||
// For some unknown reason the integer value 0 is sometimes stored in the session
|
||||
// variable. It's not clear how this can happen.
|
||||
if (isset($result) && !is_string($result))
|
||||
{
|
||||
trigger_error("Stored token is of type " . gettype($result) . ", value $result", E_USER_WARNING);
|
||||
$result = strval($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Convert a file size to bytes
|
||||
// See https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
|
||||
private static function convertToBytes(string $size) : int
|
||||
{
|
||||
// Split the size into value and units (if any)
|
||||
$values = preg_split('/(?<=[0-9])(?=[^0-9]+)/i', $size);
|
||||
|
||||
if (count($values) == 2)
|
||||
{
|
||||
$result = intval($values[0]);
|
||||
switch ($values[1])
|
||||
{
|
||||
case 'G':
|
||||
$result = 1024 * $result;
|
||||
// Fall through
|
||||
case 'M':
|
||||
$result = 1024 * $result;
|
||||
// Fall through
|
||||
case 'K':
|
||||
$result = 1024 * $result;
|
||||
return $result;
|
||||
break;
|
||||
default:
|
||||
// Unrecognised suffix
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return intval($size);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user