SaaS系统最危险的数据安全问题之一,并不是数据库被黑客攻破,而是系统本身在正常运行过程中,把A客户的数据返回给了B客户。
这种问题在多租户系统里尤其棘手。攻击者甚至不需要突破防火墙,也不需要拿到数据库账号,只要修改请求中的某个ID,或者利用一个没有正确应用租户边界的API,就可能读取其他企业的客户、订单、文件、财务记录甚至管理员信息。
因此,SaaS的数据隔离不能简单理解为“每个客户一个数据库”,也不能把RBAC、加密和网络隔离混在一起。真正需要建立的是一条完整的租户安全边界:用户身份属于谁、当前操作针对哪个租户、业务对象属于哪个租户、数据库查询允许访问什么、缓存和文件系统如何隔离,以及异步任务和微服务之间如何继续传递这个租户上下文。
最常见的设计是共享数据库、共享表,在业务表中增加tenant_id。例如orders表不仅保存订单自身的数据,还保存organization_id或tenant_id。用户登录后,系统根据membership关系确定用户属于哪些组织以及相应角色。一次请求进入系统以后,服务端必须建立可信的tenant context,而不是直接相信浏览器传来的tenant_id。
例如请求中带有organization_id=1001,并不意味着当前用户就有权访问1001。服务器首先需要确认当前认证用户是否属于1001,再根据其membership、role以及具体资源权限决定能否读取该对象。浏览器提交的tenant_id只能作为请求参数,不能成为授权依据。
这也是很多多租户系统发生越权的根源。开发人员可能写出类似SELECT * FROM orders WHERE id = ?这样的查询,然后认为因为用户已经登录,所以查询是安全的。但订单ID本身通常并不包含租户授权信息。如果攻击者把自己的订单ID换成另一个企业的订单ID,而后端只按照id查询,就可能形成典型的IDOR,也就是不安全的直接对象引用。
更可靠的设计应该让数据访问层天然携带租户边界。例如查询订单时,同时约束资源所属的tenant_id,而不是要求每一个业务开发人员在几十个SQL语句里自行记住这一规则。更进一步,可以在数据库层使用Row-Level Security等机制,让数据库本身参与租户边界 enforcement,从而降低某一个SQL遗漏过滤条件所造成的风险。
这并不意味着所有SaaS都必须采用RLS。应用层强制tenant scope同样可以成立,但大型系统需要考虑如何让这个规则具有一致性。如果租户过滤只是散落在Controller、Service和Repository代码中的若干WHERE条件,随着团队扩大和代码量增加,遗漏的概率也会增加。
数据库架构则是另外一个层次的问题。
共享数据库共享表的成本最低,也是大量SaaS产品采用的方式。所有客户的数据位于同一套表结构中,通过tenant_id划分逻辑边界。优点是数据库数量少、迁移方便、连接池管理简单,也便于统一分析和运维;缺点是隔离主要依赖应用和数据库层面的访问控制,一旦出现租户过滤漏洞,影响范围可能很大。
共享数据库、独立Schema可以进一步把对象命名空间分开,但它并没有自动解决授权问题。如果应用拥有访问所有Schema的数据库账号,而业务代码仍然可以任意切换Schema,那么Schema本身并不是完整的安全边界。
独立数据库则提供更强的数据边界。不同客户可以拥有不同数据库,甚至部署在不同实例或不同区域。这样做适合大型客户、强隔离需求、数据驻留要求或者需要独立备份恢复策略的场景,但代价是数据库连接管理、迁移、监控、备份、版本升级和故障处理都会明显复杂化。
因此,SaaS真正成熟的做法往往不是在“所有客户共用”和“每个客户独立数据库”之间二选一,而是采用混合架构。普通租户使用共享数据库,大客户或者具有特殊安全、合规和性能要求的租户迁移到独立数据库。系统从设计之初就应该让tenant能够从shared storage迁移到dedicated storage,而不是等客户规模扩大以后重新设计整个数据层。
数据隔离也绝不能停留在数据库。
缓存是非常容易被忽略的一层。假设Redis缓存键设计成user:123,那么用户本身可能没有问题;但如果业务对象是租户级数据,缓存键就必须能够表达租户边界。例如tenant:1001:order:8899与tenant:2002:order:8899必须是两个完全不同的缓存空间。否则数据库查询虽然做了tenant_id过滤,缓存命中以后却可能把另一个租户的数据直接返回。
对象存储同样如此。客户上传的合同、图片、报表和附件往往不会进入MySQL,而是进入对象存储。此时不能因为数据库已经隔离,就认为文件天然安全。对象Key、Bucket策略、预签名URL生成逻辑以及文件下载API都必须继续执行租户授权。
搜索系统也是一个经常发生问题的地方。SaaS可能把数据同步到Elasticsearch或其他搜索引擎,数据库里的tenant_id过滤如果没有同步进入搜索索引,用户搜索时就可能跨租户返回结果。因此搜索文档本身需要包含明确的租户字段,查询层也必须执行租户过滤。
异步任务和消息队列同样属于租户边界。
例如客户A创建一个报表,系统把任务放进队列,由Worker稍后生成。如果任务消息只包含report_id,而Worker查询数据库时没有重新建立租户上下文,那么一个错误的任务ID就可能让Worker处理其他租户的数据。更危险的是,后台Worker往往拥有比普通API更高的数据库权限,因此它必须有自己的授权和租户隔离机制。
微服务架构会让问题进一步复杂化。一个请求可能经过API Gateway、用户服务、订单服务、支付服务、文件服务和报表服务。不能因为入口服务验证过tenant_id,就默认后面的服务永远安全。租户上下文需要以可信方式沿服务调用链传递,同时每个拥有数据访问能力的服务都应该验证自己的授权边界。
这也是为什么“使用Service Mesh就能实现租户隔离”是一个容易误导的说法。Service Mesh非常适合解决服务身份、mTLS、流量策略和网络层访问控制,但它并不知道某一行订单属于哪个客户。真正的业务数据隔离仍然需要应用和数据层完成。
RBAC解决的则是另一类问题。
角色决定用户能够做什么,例如管理员可以管理成员,财务人员可以查看账单,普通员工只能查看自己的业务数据。但RBAC本身不能替代tenant isolation。一个用户即使拥有“管理员”角色,也应该只在自己所属的组织范围内拥有管理员权限。
因此,成熟的模型通常是User、Organization、Membership、Role和Resource分别建模。一个User可以属于多个Organization,在A组织中是管理员,在B组织中只是普通成员。请求进入系统以后,必须同时确定用户身份、当前租户、角色以及资源所属租户。
这比简单地在users表增加一个tenant_id更加灵活。后者实际上把一个用户永久绑定到一个客户,而多组织SaaS通常需要允许同一个用户加入多个企业。
加密解决的是机密性问题,而不是租户授权问题。
数据库采用AES-256加密,并不能阻止应用程序错误地执行SELECT并把A公司的数据返回给B公司。如果两个租户使用同一个数据库账号访问同一张表,那么数据库看到的仍然是同一个应用身份。
因此,静态数据加密、传输层TLS、密钥管理和租户授权应该分开考虑。对于需要更高隔离等级的客户,可以进一步使用独立密钥、Envelope Encryption或者每租户密钥策略,但密钥设计同样需要考虑密钥生命周期、轮换、权限和审计,而不是简单地写一句“AES-256即可”。
网络隔离也属于类似问题。
VPC、Security Group、ACL、VLAN等可以限制网络层面的通信,但它们并不会自动知道某条SQL查询属于哪个租户。对于绝大多数共享SaaS架构,并没有必要为了每一个租户创建一套VPC。这样做会迅速增加网络、路由、DNS、监控和运维复杂度。
网络隔离真正有价值的场景,是不同安全域之间需要降低横向移动风险,例如数据库与应用层隔离、生产环境与管理网络隔离,或者对某些独立租户进行更高等级的基础设施隔离。
资源隔离则主要解决另一个问题,也就是Noisy Neighbor。
某个租户突然提交大量任务,不应该把CPU、内存、连接池、线程池或者队列全部占满,从而影响其他客户。Kubernetes中的Namespace、ResourceQuota和LimitRange,以及容器级CPU和内存限制,都可以参与资源治理。但资源隔离不能代替数据隔离。一个Pod即使只获得1个CPU,也不代表它只能看到自己的数据。
真正成熟的多租户系统,还必须把租户边界贯穿到日志和审计系统。
审计记录应该至少能够回答谁在什么时间,以什么身份,在什么租户上下文中,对哪个资源进行了什么操作。日志系统本身也必须考虑租户边界,因为把数据库查询结果或者访问Token直接写进日志,可能造成另一次敏感信息泄露。
同时,不能把所有日志字段简单脱敏后就认为安全。更重要的是控制谁能够查询生产日志,以及日志查询本身是否受到权限和审计约束。
测试则不能只做“登录后看看页面有没有显示别人的数据”。
真正的租户隔离测试应该主动模拟越权。使用租户A的身份请求租户B的资源ID,修改URL中的对象ID,修改JSON中的tenant_id或organization_id,交换不同租户的文件Key,测试缓存命中,测试搜索接口,测试批量查询接口,测试导出接口,测试报表任务以及后台异步Job。
尤其需要测试那些“不常用”的API,因为数据泄露经常不是发生在核心列表页面,而是发生在下载、导出、统计、批量操作、搜索、Webhook、后台任务或者旧版本API中。
还应该进行负向测试,也就是明确验证“这个请求必须返回拒绝”。不能只测试管理员能够访问什么,更应该测试普通成员不能访问什么、租户A不能访问什么,以及已经离职的membership是否立即失效。
对于共享数据库系统,数据库查询层还应该进行自动化安全测试。例如建立两个测试租户,制造相同业务ID、相同文件名和相似数据,然后通过整个API集合进行交叉访问。如果任何一个接口能够让租户A得到租户B的数据,测试就应该失败。
SaaS的数据隔离最终不是某一个数据库选型问题,而是一条贯穿身份、授权、数据库、缓存、对象存储、搜索、消息队列和微服务的安全边界。
数据库可以共享,但租户上下文不能共享;缓存可以共享基础设施,但缓存键不能混淆租户;对象存储可以共享平台,但下载授权必须重新验证;消息队列可以共享集群,但任务处理必须携带并验证租户上下文。RBAC负责角色权限,RLS可以把行级边界下沉到数据库,TLS负责传输机密性,KMS负责密钥生命周期,它们分别解决不同问题,不能互相替代。
真正成熟的SaaS架构,并不是把每个客户都塞进一个独立数据库就宣称“安全”,而是能够明确回答每一次数据访问到底经过了哪一道租户边界。只要系统能够让开发人员绕过这条边界,某一个遗漏的WHERE条件、错误的缓存Key、未经授权的文件URL或者后台任务,都可能把多租户系统变成一场数据泄露事故。安全的核心不是让租户彼此“看起来分开”,而是让跨租户访问在架构、代码、数据库和测试层面都变得困难,并且能够被持续验证。
SaaS 用户数据如何隔离,避免一个客户看到另一个客户的信息
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
分享 Facebook | X | WhatsApp | LinkedIn
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP