SaaS系统最危险的权限漏洞,往往不是登录系统被攻破,而是用户已经合法登录之后,却能够访问自己本来不应该看到的数据,或者执行自己没有权限执行的操作。攻击者不一定需要破解密码,也不一定需要伪造JWT,只要把正常请求中的资源ID、组织ID或者操作参数换成另一个用户的数据,后端如果没有重新判断访问关系,就可能出现典型的越权漏洞。比如普通员工把 /api/orders/10021 改成 /api/orders/10022,如果后端只检查“这个用户已经登录”,而没有检查订单10022是否属于他的组织以及他是否有权查看,就已经形成了严重的数据越权。
因此,SaaS权限系统不能设计成某一个Controller前面放一个权限判断就结束,而应该把身份、权限、资源范围和业务状态分开处理。身份验证解决的是“你是谁”,授权解决的是“你能做什么”,资源授权解决的是“你能对哪一个对象做什么”,租户隔离解决的则是“这个对象是不是属于你所在的组织”。这几个概念混在一起,是很多SaaS权限漏洞的来源。
用户请求进入系统之后,第一道检查应该是身份认证。服务器需要验证Access Token、Session或者其他认证凭证是否有效,包括签名、有效期、issuer、audience以及必要的scope等信息。OAuth 2.0本身主要解决授权框架和访问令牌的使用问题,并不意味着拿到Access Token之后就自动拥有某个SaaS资源的访问权。JWT中的role、scope或者其他Claim也不能替代服务端授权判断,因为这些字段描述的是身份和授权上下文,而不是某一条业务记录的所有权。
这一层适合完成认证以及比较粗粒度的权限预检查,例如某个API要求用户具有invoice:read权限,网关或者认证中间件可以提前拒绝完全没有该权限的请求。但这种检查不能成为最终授权依据。真正关键的判断仍然应该发生在业务服务内部,因为只有业务服务通常知道当前资源属于哪个组织、资源当前是什么状态,以及这个用户和资源之间到底是什么关系。
例如一个项目管理SaaS可能规定,普通成员只能查看自己所在项目,项目管理员可以编辑项目,组织管理员可以删除项目,但已经归档的项目禁止普通管理员删除。这里仅仅判断用户有没有project:delete远远不够。服务端还必须取得目标项目,确认项目属于当前组织,再根据用户在这个组织中的Membership、Role以及项目本身的状态决定是否允许操作。
这也是SaaS授权设计中最容易被低估的一点:权限不是简单的“用户拥有某个Role”。更加准确的模型应该是User、Organization、Membership、Role、Permission和Resource之间的关系。一个用户可以同时属于多个组织,在组织A中是Owner,在组织B中只是普通Member。因此,JWT里即使保存了某个角色,也不能直接把这个角色当成所有组织中的永久权限。服务端必须根据当前组织上下文重新确定用户的Membership以及授权范围。
对于多租户SaaS,数据访问层是防止越权最重要的第二道防线。业务表通常应该携带organization_id或者tenant_id,查询数据时不能只按照资源主键寻找记录,而应该同时限定租户范围。例如查询订单时,不能简单执行“按照order_id查询”,而应该在数据访问层确保目标订单属于当前授权的organization。这样即使攻击者把URL中的ID从自己的订单换成其他组织的订单,查询也无法返回那条记录。
这种设计还有一个非常重要的工程意义:授权范围应该尽可能靠近数据访问边界,而不是完全依赖调用方记得“先检查一次权限”。如果几十个业务接口都直接调用findById($id),其中某个开发人员忘记进行租户过滤,就可能产生一个严重的IDOR,也就是不安全的直接对象引用漏洞。把租户条件封装进Repository、Data Access Layer、查询构建器,或者使用更严格的数据访问策略,可以降低这种遗漏发生的概率。
不过,数据库查询加上organization_id并不等于完整授权。假设用户确实属于这个组织,他仍然可能没有删除某条记录的权限。数据隔离解决的是“数据是不是你的租户的数据”,业务授权解决的则是“你在这个租户里有没有权操作这条数据”。两者不能互相替代。
数据库本身还可以作为额外的安全边界。以PostgreSQL为例,Row-Level Security可以把租户或者用户的数据访问规则进一步下沉到数据库层。当应用层出现查询遗漏时,数据库策略仍然可以限制返回的数据范围。这种设计尤其适合安全要求较高的系统,但也会增加数据库策略、连接上下文、事务和调试的复杂度,因此应该根据系统规模和安全等级选择,而不是所有SaaS项目都机械启用。
字段级保护也应该区别对待。用户密码本身不应该作为普通业务字段返回给前端,密码应该使用Argon2id、bcrypt等单向密码哈希存储,而不是采用可逆加密。类似API密钥、Refresh Token、内部安全凭证等敏感数据,也不应该因为“当前用户已经通过权限检查”就直接返回完整内容。授权不仅决定能不能访问一条记录,也应该决定哪些字段可以被读取。
真正复杂的权限判断通常出现在业务逻辑层。例如用户可以编辑自己创建的草稿,但不能编辑其他人的草稿;部门经理可以批准本部门的报销,但不能批准自己的报销;管理员可以删除项目,但项目进入结算状态后删除操作必须禁止。这些规则无法单纯依靠RBAC解决,因为RBAC回答的是角色拥有什么权限,而业务授权还需要考虑资源、关系、状态和上下文。
因此,比较成熟的授权函数通常不是简单的hasPermission("delete"),而更接近“当前Actor是否允许对指定Resource执行Action”。系统需要同时考虑用户身份、组织Membership、角色权限、资源所属组织、资源所有者、资源状态以及必要的业务条件。规则非常复杂时,可以进一步引入Policy Engine或者ABAC等模型,但这并不意味着所有项目都应该一开始就建立一个庞大的策略平台。对于大多数SaaS,RBAC结合资源级授权已经能够覆盖相当多的场景。
前端权限控制则属于体验层,而不是安全边界。前端可以根据权限隐藏“删除”“导出”“管理成员”等按钮,让用户看到更合理的界面,但攻击者完全可以绕过浏览器直接发送HTTP请求。因此,前端把按钮隐藏掉并不能构成安全控制。服务器必须重新判断请求者身份和权限,不能相信浏览器传来的role、permission、is_admin或者类似字段。
同样的原则也适用于组织ID。很多多租户SaaS会在URL或者请求体中出现organization_id,这没有问题,但服务器绝不能因为客户端提交了organization_id=123就认为当前用户属于组织123。正确做法是服务器根据认证身份查询Membership,确认用户确实属于该组织,然后建立当前请求的授权上下文。客户端提供的是“目标”,不是“权限证明”。
API Gateway在大型SaaS中可以承担认证、Token验证、基础限流以及粗粒度访问控制,但不能把所有业务授权都塞进网关。网关不知道订单是不是已经付款,也不知道项目是不是属于当前用户管理的部门。如果把复杂业务规则全部放进网关,最终会形成一个巨大而难以维护的权限中心。比较合理的职责划分是,网关处理通用的身份和流量控制,业务服务负责资源授权和业务规则,数据访问层负责租户范围和数据隔离。
微服务之间也是同样的道理。mTLS能够证明“这是哪个服务建立的连接”,但不能自动证明“这个服务有权读取所有客户数据”。服务身份认证和业务授权是两个不同的问题。订单服务调用用户服务时,可以通过服务身份、OAuth Token或者其他服务间授权机制确认调用者身份,再根据接口权限决定是否允许访问。敏感操作还应该留下审计记录,包括调用方身份、租户、操作对象、操作类型、时间、结果以及失败原因等必要信息。
这里也容易出现一个架构误区,就是认为Service Mesh可以解决SaaS全部权限问题。Service Mesh擅长服务间通信、安全连接、流量管理和部分策略控制,但它并不知道某个用户是否有权删除某一张订单。对于规模不大的SaaS,专门为了权限控制引入复杂的Service Mesh并没有必要。真正应该优先解决的是身份模型、租户隔离、资源授权以及审计。
CORS也不能被当作越权防护机制。CORS主要控制浏览器是否允许一个Origin读取跨域响应,而不是判断某个用户有没有业务权限。攻击者完全可以使用服务器、脚本或者其他HTTP客户端绕过浏览器的CORS限制。与此同时,CSRF属于另一类问题,需要结合Cookie认证方式、SameSite策略、CSRF Token等机制处理。把CORS和CSRF、授权混为一谈,会让安全设计出现明显漏洞。
对于高风险操作,还需要考虑权限变化后的实时生效问题。比如管理员刚刚撤销某个员工的权限,如果系统使用长生命周期Access Token或者缓存了用户权限,撤销是否能够立即生效?如果用户仍然持有有效Session或者Refresh Token,系统是否需要主动失效?这些问题往往比简单的RBAC表结构更加棘手。权限缓存需要考虑TTL、版本号、主动失效以及多实例之间的一致性,否则数据库里的权限已经撤销,Redis中的旧权限仍然可能让用户继续访问敏感资源。
审计日志则是权限体系的另一部分。系统不仅要记录“谁登录了”,还应该能够回答谁在什么时候以什么身份访问了哪个组织中的哪个资源、执行了什么操作,以及操作最终成功还是失败。对于删除客户、导出大量数据、修改组织管理员、生成API密钥等高风险动作,还应该能够建立完整的审计链。日志不能记录密码、Access Token、Refresh Token等敏感凭证,否则安全系统本身可能成为新的泄露源。
从工程角度看,SaaS越权防护并不是在API入口加一个checkPermission()这么简单。比较稳健的结构是认证系统负责确认用户身份,Membership负责确定用户属于哪个组织,RBAC或其他授权模型负责确定允许执行哪些Action,业务服务负责判断具体Resource是否满足操作条件,数据访问层负责强制租户范围,数据库策略在必要时提供第二道隔离,网关和服务间安全机制负责保护系统边界,审计系统则负责记录关键授权事件。
这样的设计还有一个很重要的特点,就是即使某一层出现遗漏,也不应该立即导致整个租户的数据暴露。API入口漏掉一次粗粒度权限检查,数据访问层仍然应该阻止跨租户查询;业务代码忘记检查某个角色,资源授权策略仍然应该拒绝不符合关系的操作;前端即使完全被攻击者绕过,服务器仍然掌握最终授权权力。SaaS权限体系的成熟程度,最终不是看权限表有多少字段,而是看系统能不能在身份、组织、资源和业务操作之间建立一条无法轻易绕开的授权边界。
对于绝大多数SaaS产品,RBAC并不是终点,但也没有必要一开始就把系统做成复杂的策略引擎。先把User、Organization、Membership、Role、Permission和Resource关系设计清楚,再把租户隔离落实到数据库查询和数据访问层,最后针对所有修改、删除、导出和管理类操作建立资源级授权,通常比堆叠各种安全产品更加有效。越权防护真正需要解决的,是让任何一个请求都无法仅凭“我已经登录”就获得它不应该拥有的数据和操作能力。
SaaS 系统如何防止用户越权访问,权限检查应该放在哪些环节
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP