一、背景介绍
在当今数字化的时代,数据泄露问题层出不穷,往小了说会威胁到个人隐私,往大的说可以威胁到国家安全。每年因数据泄露造成的事故一直还在上升,我们不禁要反思一下,这是为什么?
数据工单导出 是业务人员与 DBA 很常见的工作内容,但也是一个很容易出现数据泄露的行为。本文将分享某知名二手交易平台在数据导出工单改造 过程中的实践经验,希望能够提升大家对于数据安全的认识,共同探索更加完善的数据保护策略。
以MySQL的数据导出工单为例,主要想分享的是第四部分,数据分类分级的判定。
二、数据导出工单1.0
1、数据导出工单的作用
我们先思考一下为什么需要数据导出工单这个功能?
这个功能主要是面向研发同学,即数据的操作者。我们可以从下面几个角度分析:
背景:业务所能操作的机器都没有
mysql客户端,所以业务想查看线上的数据只能找DBA协助,目前我们DBA提供两个通道,其一是数据查询平台,数据查询平台每次只能查询少量数据,且不可导出(复制),其二是数据导出工单,数据导出工单允许业务拿到数据。
- 问题调试
有时候,当线上服务出现故障或异常行为,研发同学需要分析实际运行中的数据来定位问题根源,这样可以帮助他们更准确地重现和诊断问题。
- 测试验证
一般情况下,新功能上线前需要在沙箱环境进行验证,这时候往往需要使用真实的线上数据来进行全面测试。
- 数据分析
良好的数据分析可以有助于产品的迭代和发展。
- 数据备份
备份是数据保护的一个措施,这也是一个很好的习惯。
基于上述的合理需求,作为DBA,我们应该予以协助完成。但是,手动处理过数据导出操作的DBA都应该知道,这种工作重复性很高,又比较繁琐,耗时可能还很长。
如果偶尔处理一两个需求尚且能接受,但是随着业务的不断壮大,我们管理的数据库集群越来越多,数据量越来越大,一个月几十个上百个这种需求也很正常。
我相信,任何一个正常的DBA面对这种情况都能疯掉,那么数据导出工单就应运而生了。
当然,这个工单的作用也不止于此,还有一个重要的作用,它可以明确记录每个操作的时间、执行人及数据内容,以便于事后审查与责任划分。
至此,我们把业务的需求明确了,那就可以开始干活了。
2、数据导出工单1.0
这里只是简单的介绍一下该工单的处理流程,后面2.0的时候再详细介绍一下。
- 业务按需编写
select语句并提交工单 - 生成审批流(固定审批流)
- 目标集群的负责人审批
- 执行数据导出操作,并返回下载的
URL - 下载数据文件
不知道大家有没有发现这个处理流程存在的安全问题?
- (1)审批流存在重大安全问题
固定审批流存在重大安全问题,不管被导出的数据是否敏感,只要集群负责人的审批即可。其次,业务通过这个工单可以将线上的数据全部导出来。
可以发现,集群负责人权限太大,与其能承担的责任相当不匹配,风险不可控。
- (2)审批人在审批的时候没法看到导出的目标数据的情况
形成工单流后,审批人没法看到目标数据的具体情况,比如该导出的数据量条数,数据是否包含敏感数据等。这些信息只能通过跟提交人沟通获取,假如说,工单提交者对实际情况进行隐瞒,审批人是没法发现的。
- (3)URL未做鉴权
生成的URL未进行鉴权,即只要有查看工单详情的人员都可以通过点击该URL进行下载。
- (4)数据文件未加密
数据文件未加密,即下载以后可直接查阅。
一梳理分析才发现,这个工单存在的问题真不少。
其实这些问题不是我们DBA发现的,或者说我们在开发之初就没考虑那么多,我们认为用户都是好人,要不然当初也不能这么开发。这个工单在2020年9月份上线,直到2023年年初的时候,安全组的同学找到我们DBA,希望我们严格把控线上数据的流出,这才有了上述几点问题的总结。
最后,我们开始了长达一年多的整改。
三、数据导出工单2.0
2.0版本的流程如下:
- 业务按需编写
select语句并提交工单 - 数据分类分级判定
- 生成审批流(动态审批流)
- 目标集群的负责人审批,(可能还需要直属领导审批,部门领导,集群所属部门主管审批,
CEO审批) - 执行数据导出操作,并返回下载的
URL(点击下载后1小时后失效) - 下载数据文件
可以看到这里多了一个数据分类分级的判定,这个主要做的事情就是将业务的SQL语句进行解析,将表名,字段名等信息都拿到,然后判断是否是敏感数据,另外会通过执行计划预估导出的数据行数。最终会将这些信息返回到工单详情页,审批人可以看到,可以作为审批参考。
预估行是通过执行计划得到,是不准确的,只能作为参考。如果SQL是全表扫描,这个值跟实际情况可能相差很大。
数据分类分级判定是我们自己的一个内部系统,会定期扫描线上的表和字段,抽样检查样本数据,最后对表及字段形成敏感等级,如果最敏感级别的数据将可能需要CEO审批。
要实现这个数据分类分级判定功能我们需要解决三个大问题,这部分将在下一节介绍。
四、数据分类分级判定
说了那么多废话,终于来到了正文。
这部分是想介绍一下我们是怎么实现数据分类分级判定,要实现这个功能需要解决三个大问题:
1、获取导出语句的表名和字段名
大家先来思考一下这个需求好做吗?我的回答是,非常难实现,这无异于是实现了SQL解析的功能。
MySQL所有SQL语句类别里面,select一定是最复杂的,所以想实现获取表名,字段名是十分有难度的。
有些同学可能就会说了,有很多开源工具都支持SQL解析,直接拿来用就好了。这么说也没问题,但是即便使用了开源工具,还是会存在很多问题,归根结底还是因为select太复杂。
下面就以python代码为例。python有个sqlparse模块,是SQL的解析库,但是使用过程中遇到很多问题,下面列举一些我遇到的问题:
(1)连接查询
使用连接查询的场景,包含不限于["inner join","left join","right join","join",","],这些会对结果产生影响,可能无法获取预期内的表名列表。
from后面的逗号也是一种特殊的连接查询语法。
(2)union查询
取并集操作也会对结果产生影响,可能无法获取预期内的表名列表。
(3)子查询问题
不管是select后面使用子查询,还是from后面使用子查询,还是where后面使用子查询,都会对获取表名列表结果产生影响,可能无法获取预期内的表名列表。
多层子查询又是更加复杂的问题。
(4)四则运算问题
select后面使用了运算符,如常见的加减乘除会对获取字段列表结果产生影响,甚至大部分情况下都是结果未达到预期。
(5)函数问题
select后面使用了函数,如常见的聚合函数,字符串处理的函数等都会对获取字段列表结果产生影响,甚至大部分情况下都是结果未达到预期。
(6)case when
select后面使用了case when,会对获取字段列表结果产生影响,甚至大部分情况下都是结果未达到预期。
(7)使用别名问题
select后面的字段使用了别名,会对获取字段列表结果产生影响,甚至大部分情况下都是结果未达到预期。
(8)SQL语句很长,解析很慢问题
如果业务的SQL语句很长,好几万个字符(in里面成千上万个值),这种场景解析超级慢,使用体验很不友好。
(9)奇怪的语法
如果select语句使用奇怪的语法,如正常语法是db.table,然后奇怪的语法是db. table,即db.后面多一个空格,在MySQL里面是不会报错,能正常执行。但是解析的时候会有问题,最终会对获取表名列表结果产生影响,甚至大部分情况下都是结果未达到预期。
对上述提到的场景,直接使用sqlparse是没法获取到表名字段名等信息,需要对sqlparse的解析结果做场景适配,具体的场景测试这里就不一一展示了,直接上最终的代码。
有兴趣想了解更加详细的介绍/解释可以查阅代码的注释。
git clone https://gitee.com/mo-shan/get-tab-clo.git
该项目依赖
sqlparse,需要自行安装该模块,另外在python 2.7.5 3.7.4 都测试通过。
python GetResult.py
{"col": ["sql", "id", "db_name", "db_port", "db_type"], "tab": ["tb_export_data_new"], "sql": "select `sql`,id,db_name,db_port,db_type from tb_export_data_new where workorder_id = 1"}
{"col": ["name", "order_id"], "tab": ["tb_export_data_new", "t2"], "sql": "select t1.name,t2.order_id from tb_export_data_new t1 join t2 on t1.id = t2.id where workorder_id = 1"}
{"col": ["addr", "name", "order_id"], "tab": ["t3", "tb_export_data_new", "t2"], "sql": "select t1.name,t2.order_id,(select t3.addr from t3 limit 1) from tb_export_data_new t1 join t2 on t1.id = t2.id where workorder_id = 1"}
{"col": ["sql", "id", "db_name_old", "db_port", "db_type", "sql", "db_name"], "tab": ["tb_export_data_old", "tb_export_data_new"], "sql": "select `sql`,id,db_name_old,db_port,db_type from tb_export_data_old where workorder_id = 1 union select `sql`,id,db_name,db_port,db_type from tb_export_data_new where workorder_id = 2"}
至此获取表名,字段名列表的工作就算完成了。
2、获取导出语句的执行计划
这部分工作倒是没什么特别复杂的,就说几个注意事项:
(1)MySQL版本问题
5.6和5.7 8.0的的执行计划结果不一样,需要注意适配。
(2)预估行问题
对于使用子查询或者连接查询的场景,预估行需要求乘积。
多表连接需要多次乘积。
(dba:4202)@[dbzz_new_dbversion1]>desc select * from t t1 join t t2 on t1.id = t2.id where t1.id = 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 849643 | 10.00 | Using where |
| 1 | SIMPLE | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 849643 | 10.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)
(dba:4202)@[dbzz_new_dbversion1]>
比如这里的案例需要
849643 * 849643,另外,如果这里解析异常可以给一个默认值,比如10000。
(3)TiDB的执行计划
TiDB和MySQL的的执行计划结果不一样,需要注意适配。
例外需要注意,TiDB的rows可能是float型,需要做一下类型转换。
3、获取表和字段的敏感等级
这部分工作每个公司的情况都不一样,且涉及到安全问题就不展开细说了,大致介绍一下实现的逻辑。
敏感等级的判定分两个维度,即:命名和实际存储的数据。
(1)命名规则判定
这里需要我们定期去扫描业务表,获取到表名,字段名。需要注意的是,我们跑完全量样本后,后续每次只需要扫描增量即可。
想实现增量扫描需要做到将新建表,删表,新增字段,删除字段这样的需要告知扫描系统即可,即:需要有建表/删表/改表工单。
比如业务命名了一个user表,我们就视为敏感类型的表。
再比如业务命名了一个xxx表,其中有个username passwd等字段,我们也会视为敏感类型。
(2)数据样本抽样判定
这里也需要我们定期去扫描业务表,获取到样本数据。需要注意的是,我们跑完全量样本后,后续每次只需要扫描增量即可。
比如业务命名了一个xxxx但是存储的是18位或者11位数字这种数据,我们就有理由怀疑是存的是身份证号或者手机号,所以也会被视为敏感类型。
具体的匹配规则需要根据各自公司的实际情况进行设定。
4、数据分类分级判定
完成上面三个事项后,导出工单会根据执行计划的结果的预估行及数据敏感等级综合评估,然后给一个最终等级。
之所以需要依赖导出的数据条数是参考了相关法律法规。
(一)致使泄露行踪轨迹信息、通信内容、征信信息、财产信息五百条以上的;
(二)致使泄露住宿信息、通信记录、健康生理信息、交易信息等其他可能影响人身、财产安全的用户信息五千条以上的;
(三)致使泄露第一项、第二项规定以外的用户信息五万条以上的;
(四)数量虽未达到第一项至第三项规定标准,但是按相应比例折算合计达到有关数量标准的;
等级越高,就需要更高级的领导审批,最高需要CEO审批(申请人直属领导 --> 集群负责人 --> 集群的部门领导--> 安全组 --> 集群的部门领导的直属领导 / CEO)。这里需要注意,有可能会出现同一个人出现在不同的审批节点上,针对这种情况需要自动通过。
比如A申请了本部门的一个数据导出工单,A的直属领导是B,同时B又是部门领导,这时候一级审批和三级审批都是B,这时候B只需要在一级审批的时候审批,等二级审批后到三级审批需要自动通过。
如果判定失败可以给一个默认数据等级,并发通知给
DBA。
五、总结
数据导出工单应该说是一把双刃剑,既方便业务也方便DBA,但是也带来了很多安全问题,我们现在所做的事情就是对其进行收口,尽可能的做到防止未经授权的数据泄露出去,也可以起到责任追溯的作用。
我们的目的就是希望可以牵引业务在使用数据导出功能的同时符合安全管控规范,有效提升数据防泄漏能力,在改造以后现在将近运行了大半年,数据导出工单数量在同周期内下降了57%,且工单撤回率提升25%。
以上,仅供参考。