在禁用内置安全机制的 MVC 项目中,如何提高安全性?
可以降低部分风险,但仍需调整这些限制
无 salt 的 SHA1、过短的最低密码长度、不受限制的登录尝试,以及长期有效的认证凭据,都会增加风险,包括密码被猜中、密码库泄露后被破解,以及会话被盗用。应用保存着财务及个人记录,这些问题应纳入上线决策。
即使现有限制不变,参数化查询、服务端授权、输出编码、HTTPS 和数据库最小权限仍能降低部分风险。不过,它们无法弥补密码存储和登录防护的缺陷,也不足以证明系统适合处理真实敏感数据。
仅凭目前的描述,还不能断定应用“一定很容易被攻破”。评估时应区分已确认的设计缺陷、尚未验证的实现问题和能够复现的漏洞。
需要优先说明的问题
是否使用 ASP.NET Identity,不能单独决定应用是否安全。即使用了它,授权和数据访问也可能出错。评估现有方案时,应具体说明它提供了哪些保护,又明确取消了哪些保护。
| 现有设计或限制 | 具体风险 |
|---|---|
| 使用无 salt 的 SHA1 保存密码 | 相同密码产生相同哈希;数据库泄露后,攻击者可以快速进行离线猜测,并复用预计算结果 |
| 最低密码长度很短,且没有登录尝试限制 | 弱密码可以被持续在线猜测;即使换用安全的密码哈希,也不能解决这个问题 |
| 认证凭据长期有效 | 凭据一旦泄露,攻击者可能长期使用;实际影响还取决于服务端能否撤销会话 |
| 敏感业务字段以明文保存 | 拥有相应读取权限的数据库账号、被窃取的备份,或被成功利用的数据读取漏洞,都可能直接暴露这些信息 |
| 使用自定义 Session 和角色逻辑 | 如果缺少统一检查,容易出现遗漏授权、权限撤销不生效或跨机构访问等问题 |
还需要澄清几个概念:
- SHA1 是哈希算法,不是加密算法。用它保存密码的主要问题是计算过快、没有 salt,不能简单归结为 SHA1 的碰撞问题。
- Data Annotations 常用于模型验证;经典 ASP.NET MVC 中的
[Authorize]是授权过滤器,两者用途不同。禁用 Data Annotations 后,仍可手动验证输入。 - 身份验证查询使用的表同时保存敏感数据,本身不能证明存在漏洞。具体暴露范围取决于查询读取哪些字段,以及数据库账号能访问多少数据。
- 服务端 Session 可以保存登录状态,但仅有 Session,无法证明请求经过了可靠的身份验证。
先明确允许使用的安全能力
需要把“不能使用安全工具”具体化,逐项确认是否允许使用:
- HTTPS,以及框架提供的密码学和安全随机数 API。
- 参数化 SQL、数据库权限、视图或存储过程。
- HTML 输出编码和防伪令牌。
- 登录后更新会话标识,以及退出、改密、账号停用时撤销会话。
- 业务访问审计和异常告警。
后续措施能否实施,取决于这些能力是否被允许。如果连成熟的密码学 API 和令牌保护机制都被禁止,靠自定义方法也难以可靠地替代它们。
把登录限速移到代理层,仍需重新讨论政策是否允许;给密码附加固定字符串,也不能替代每个密码独立的随机 salt。这两种做法都不能直接视为符合现有限制的解决方案。
在允许范围内,先完成这些改进
每次请求都执行服务端授权
登录成功确认了用户身份。读取、修改、删除或导出某条记录时,还要检查该用户是否有权执行这项操作。
涉及多个机构的应用尤其需要检查记录所属机构。用户提交的 UserId、TenantId、角色名称和隐藏字段,都不能直接作为授权依据。
授权检查应遵循以下规则:
- 默认拒绝访问,确认允许后再继续。
- 每个受保护入口都检查身份和权限,包括 AJAX、下载及导出接口。
- 检查用户对具体记录的访问权限,不能只看用户是否拥有某个角色。
- 修改角色或停用账号后,已有会话应能及时反映变化。
- 读取授权所需信息失败时,拒绝操作。
将这些规则集中到可复用的服务中,避免每个 Controller 各写一套。隐藏按钮可以改善界面,但服务端仍须执行授权检查。
使用参数化查询,并将授权条件加入数据查询
即使身份验证必须查询那张包含敏感字段的用户表,也应只读取验证所需字段,避免 SELECT *。Session 中也不应保存完整用户记录。
查询业务数据时,应同时约束记录编号和访问权限。以下代码假设项目使用经典 ASP.NET MVC 和 System.Data.SqlClient,表名及字段名需按实际结构调整:
const string sql = @"
SELECT r.Id, r.Amount
FROM FinancialRecords AS r
WHERE r.Id = @RecordId
AND EXISTS (
SELECT 1
FROM UserTenantPermissions AS p
INNER JOIN Users AS u ON u.Id = p.UserId
WHERE p.TenantId = r.TenantId
AND p.UserId = @UserId
AND p.CanReadFinancialRecords = 1
AND u.IsActive = 1
);";
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(sql, connection))
{
command.Parameters.Add("@RecordId", SqlDbType.Int).Value = recordId;
command.Parameters.Add("@UserId", SqlDbType.Int).Value =
authenticatedUserId;
connection.Open();
using (var reader = command.ExecuteReader())
{
// 没有结果时,不返回该记录。
// 有结果时,只映射响应确实需要的字段。
}
}
authenticatedUserId 必须来自已经验证的服务端登录状态,不能直接取自请求参数。示例仅涉及参数化查询和记录级授权,身份验证仍需另行实现。
更新和删除操作也应在 SQL 条件中加入相应权限约束,并检查受影响行数。如果先查询权限,再修改数据,还需考虑两步之间权限或记录归属发生变化的情况。
手动验证输入,保留输出编码
即使不能使用部分 Data Annotations,也可以在服务层检查必填字段、长度、数值范围和业务规则。例如:
if (string.IsNullOrWhiteSpace(model.Description) ||
model.Description.Length > 200)
{
ModelState.AddModelError(
"Description",
"说明不能为空,且不能超过 200 个字符。");
}
if (!ModelState.IsValid)
{
return View(model);
}
示例中的 200 应按实际业务和数据库字段约束调整。
表单模型只接收用户可以编辑的字段。角色、账号状态和机构归属等字段应由服务端决定,避免直接绑定整个数据库实体,导致越权赋值。
输出时保留 Razor 默认的 HTML 编码,不要对不可信内容使用 Html.Raw。将内容写入 JavaScript、URL 等位置时,需要使用适合该上下文的编码方式。
保护会话,防止伪造请求
在允许相关配置的前提下,应采取以下措施:
- 全站使用 HTTPS,并为认证 Cookie 和 Session Cookie 设置适当的
Secure、HttpOnly和SameSite属性。 - 登录成功和权限提升后更新会话标识,防止会话固定。
- 退出、改密、账号停用时,在服务端撤销对应会话。
- 避免把会话凭据放进 URL、日志或浏览器可读的普通存储中。
这些 Cookie 属性的作用有各自的边界。HttpOnly 可以限制脚本直接读取 Cookie,但无法阻止 XSS 利用当前登录状态发起操作;SameSite 也不能替代完整的 CSRF 防护。
修改数据的操作应使用合适的 HTTP 方法,并验证防伪令牌。如果允许使用经典 MVC 的防伪机制,可以配套使用:
@Html.AntiForgeryToken()
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Update(UpdateRecordModel model)
{
// 仍需执行输入验证、身份检查和记录级授权。
}
如果这一机制也被明确禁止,应将缺少 CSRF 防护记为待解决的风险。仅把 GET 改成 POST,无法解决这个问题。
缩小数据泄露范围
应用数据库账号只应拥有必需权限,避免使用数据库管理员账号。要求查询同一张表,不等于必须允许应用读取其中所有字段。如果条件允许,可通过受限视图或存储过程进一步限制访问。
还应处理以下泄露风险:
- 测试环境使用合成数据,避免复制真实财务记录。
- 日志不记录密码、密码哈希、认证票据、Session ID 或完整敏感请求体。
- 生产环境不向用户展示堆栈、连接字符串和 SQL 错误。
- 保护备份及导出文件,限制下载权限,并按业务需要设置保留期限。
- 对敏感响应设置适当的缓存策略。
数据库或磁盘加密能保护部分离线介质泄露场景。拥有合法读取权限的应用账号仍可查询明文,SQL 注入和越权访问也需要分别修复。
密码方案必须单独整改
密码应使用专门的密码哈希方案保存,例如 Argon2id、scrypt、bcrypt 或配置得当的 PBKDF2,并为每个密码生成独立的随机 salt。具体方案和成本参数需结合运行环境确定。
只要“不能使用 salt”的限制不变,这项基本要求就无法落实。将 SHA1 换成一次 SHA256、连续计算几次 SHA1,或增加 HTTPS,都无法解决这一问题。
安全的密码存储也无法阻止无限制的在线猜测。最低密码要求、登录尝试控制和密码哈希需要分别评估;其中一项得到改进,并不能抵消另一项仍被禁止带来的风险。
如何向主管说明问题
将担忧整理成一份简短、可复核的风险记录,每项写清楚:
- 已确认的事实,例如“密码使用无 salt 的 SHA1”。
- 攻击前提,例如“攻击者取得密码哈希副本”。
- 可能后果,例如“可进行高速离线猜测”。
- 建议改动,以及不改动时保留的风险。
可以这样表达:
当前密码库一旦泄露,攻击者可以对无 salt 的 SHA1 哈希进行快速离线猜测。HTTPS 无法缓解数据库泄露后的这种攻击。建议允许使用带独立随机 salt 的密码哈希,并重新讨论登录尝试限制。若继续保留现有方案,请由项目负责人明确决定是否接受风险,以及是否允许接入真实数据。
如果怀疑存在越权漏洞,可以在获得授权的测试环境中,用两个测试账号验证跨账号或跨机构访问。记录请求、预期结果和实际结果,不要通过访问无关客户数据来证明问题。
这类保存敏感数据的应用,建议在接入真实数据前安排独立安全评审。评审应覆盖自定义身份验证、会话撤销、机构隔离,以及密码重置等账号恢复流程。
关于继续开发和个人责任
可以继续完成获准的修复,同时准确记录尚未解决的问题。“已按需求实现”和“已经安全验证”应明确区分,发现明确缺陷后也应继续报告。
书面记录有助于说明你提出过哪些问题、由谁作出了决定,但不能据此判断或排除个人法律责任。责任取决于合同、适用法律和实际行为。涉及责任划分时,应请熟悉相关业务的律师审阅。
如果负责人坚持禁止基本防护,又要求你保证系统安全,应明确说明无法作出这一保证。是否接入真实敏感数据、是否继续由你承担这部分工作,应由有权负责的人正式决定。
备注:内容仅供参考。
