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:
人事系统开发
2026-09-09 16:55:02 +08:00
commit 1ba6efd8ed
2151 changed files with 528780 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace MRBS;
use MRBS\Form\Form;
// An Ajax function to check which of an array of time slots is invalid. (We need to do
// this server side because the client does not have sophisticated enough timezone
// handling facilities)
//
// Input parameters:
// $id the request id so that the client can match results to requests
// $slots an array of slot times in seconds from the start of the calendar day
// $day
// $month
// $year
// $tz
//
// Returns an array of slots which are invalid
require '../defaultincludes.inc';
// Check the CSRF token
Form::checkToken();
// Check the user is authorised for this page
checkAuthorised(this_page());
// Get the non-standard form variables ($day, $month and $year are standard)
$id = get_form_var('id', 'string');
$slots = get_form_var('slots', 'array');
$tz = get_form_var('tz', 'string');
$result = array('id' => $id, 'slots' => array());
foreach ($slots as $s)
{
if (is_invalid_datetime(0, 0, $s, $month, $day, $year, $tz))
{
$result['slots'][] = $s;
}
}
http_headers(array("Content-Type: application/json"));
echo json_encode($result);
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace MRBS;
use MRBS\Form\Form;
// A page designed to be used in Ajax POST calls for bulk deletion of entries.
// It takes an array of ids to be deleted as input. These are always assumed
// to be single entries. Returns the number of entries deleted, or some
// kind of string on failure (most likely a login page).
//
// If deleting lots of entries you may need to split the Ajax requests into
// multiple smaller requests in order to avoid exceeding the system limit
// for POST requests, and also the limit on the size of the SQL query once
// the ids are imploded.
//
// Note that:
// (1) the code assumes that you are an admin with powers to delete anything.
// It checks that you are an admin and so does not bother checking that
// you have rights in that particular area or room, nor does it check that
// the proposed deletion conforms to any policy in force.
// (2) email notifications are not sent, even if they are normally configured
// to be sent. Sending many thousands of emails in the space of a few
// seconds could overwhelm many mail servers, or break the usage policies
// on hosted systems.
require '../defaultincludes.inc';
require_once '../mrbs_sql.inc';
// Check the CSRF token
Form::checkToken();
// Check the user is authorised for this page
checkAuthorised(this_page());
// Check that the user is a booking admin
if (!is_book_admin())
{
exit;
}
// Get non-standard form variables
$ids = get_form_var('ids', 'string', '[]', INPUT_POST);
// The ids are JSON encoded to avoid hitting the php.ini max_input_vars limit
$ids = json_decode($ids);
// Check that $ids consists of an array of integers, to guard against SQL injection
foreach ($ids as $id)
{
if (!is_numeric($id) || (intval($id) != $id) || ($id < 0))
{
exit;
}
}
// Everything looks OK - go ahead and delete the entries
// Note on performance. It is much quicker to delete entries using the
// WHERE id IN method below than looping through mrbsDelEntry(). Testing
// for 100 entries gave 2.5ms for the IN method against 37.6s for the looping
// method - ie approx 15 times faster. For 1,000 rows the IN method was 19
// times faster.
//
// Because we are not using mrbsDelEntry() we have to delete any orphaned
// rows in the repeat table ourselves - but this does not take long.
$sql = "DELETE FROM " . _tbl('entry') . "
WHERE id IN (" . implode(',', $ids) . ")";
$result = db()->command($sql);
// And delete any orphaned rows in the repeat table
$sql = "DELETE FROM " . _tbl('repeat') . "
WHERE id NOT IN (SELECT repeat_id FROM " . _tbl('entry') . ")";
$orphan_result = db()->command($sql);
echo $result;
+8
View File
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace MRBS;
// An Ajax function to record user activity on the client side. (If there is some activity then
// this will be picked up and used by the appropriate session file).
require '../defaultincludes.inc';
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace MRBS;
// An Ajax page to update the current page in the server. Called by the client when it switches URL
// on the fly.
require '../defaultincludes.inc';
// 等保整改(2026-09-09):会话过期 / 令牌失效时静默失败,不再渲染整页错误。
//
// 背景:原版在此调用 Form::checkToken(),其校验失败时会:
// 1) session()->logoffUser() —— 把当前(可能是另一个标签页里仍然有效的)会话直接登出;
// 2) Errors::fatalError() 渲染整页“会话已过期”错误页(print_simple_header)。
// 而 style.inc 输出的 CSS 均为相对路径(jquery/...、css/mrbs.css.php),在 /ajax/
// 子目录下渲染时会被浏览器解析成 /ajax/jquery/... 等,造成大批 404(控制台噪音)。
//
// 触发场景:本接口会被每个日历页面在 page_ready 时自动 POSTjs/index.js.php),
// 属于“通知型”调用。会话按等保要求收紧为空闲 30 分钟 / 绝对 12 小时后过期,或
// 用户跨标签页重新登录后,旧页面内嵌的 CSRF token 已与服务端会话不一致,原版
// 逻辑便会触发上述整页错误渲染 + 意外登出。
//
// 本接口仅用于在服务端记录“用户当前浏览页”($_SESSION['this_page'],供返回/后退
// 功能使用),不产生任何业务状态变更,也不返回任何会被客户端处理的内容;CSRF
// 令牌在这里失配没有任何可利用的安全后果(攻击者无法伪造令牌去写他人会话)。
// 因此改为:令牌缺失或不匹配时静默退出(空 200),不做登出、不渲染页面。
// 所有真实写操作(订房、改密、用户管理等)仍由各自的 handler 页面调用
// Form::checkToken() 严格校验,安全基线不变。
$token = get_form_var('csrf_token', 'string', null, INPUT_POST);
$stored_token = session()->get('csrf_token');
// 同步记录请求里的 page 字段(供前端跳回登录后用 returl 还原回原页面)
$page_hint = get_form_var('page', 'string', null, INPUT_POST);
if (!is_string($stored_token) || !is_string($token) || !hash_equals($stored_token, $token))
{
// 等保修复 v22026-09-09):返回 403 + JSON 信号,让前端 js/index.js.php
// 识别后 location.href='index.php?returl=...' 引导用户重新登录。
// 比 v1 静默空响应更友好:用户空闲超时回来不会被静默登出/卡死。
// 安全基线不变:本接口仍无业务状态变更;JSON 中只回显 page 字段(用户自己
// 刚浏览的相对 URL,不含敏感信息)。
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'reason' => 'session_expired',
'returl' => is_string($page_hint) ? $page_hint : null,
]);
exit;
}
$page = get_form_var('page', 'string');
if (isset($page) && ($page !== ''))
{
session()->updatePage($page);
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace MRBS;
// Returns an object containing all the usernames available for use by the Select2
// tool on the edit_entry page.
use MRBS\Form\Form;
require '../defaultincludes.inc';
// Check the CSRF token
Form::checkToken();
// Check the user is authorised for this page
checkAuthorised(this_page());
// Check that the user has a legitimate reason for accessing this page
if (!can_register_others() && !is_book_admin())
{
exit;
}
$result = array();
if (method_exists(auth(), 'getUsernames'))
{
try
{
$result = auth()->getUsernames();
}
catch (\Exception $e)
{
trigger_error($e->getMessage(), E_USER_WARNING);
}
}
http_headers(array("Content-Type: application/json"));
echo json_encode($result);