爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
本文约 4000 字,预计阅读需要 15 分钟
在当今数字化的时代,数据泄露问题层出不穷,往小了说会威胁到个人隐私,往大的说可以威胁到国家安全。每年因数据泄露造成的事故一直还在上升,我们不禁要反思一下,这是为什么?
数据工单导出 是业务人员与 DBA 很常见的工作内容,但也是一个很容易出现数据泄露的行为。本文将分享 某知名二手交易平台 在 数据导出工单改造 过程中的实践经验,希望能够提升大家对于数据安全的认识,共同探索更加完善的数据保护策略。文末有奖留言。
🎁 文末有奖留言
在开始讲解实践经验之前,让我们先思考一下为什么需要数据导出工单这个功能?还是需要先交代一下需求背景。
业务人员没有直接通过 MySQL 客户端访问线上数据库的权限,想查看线上的数据只能找 DBA 协助。为此,DBA 提供两个通道:一是数据查询平台,数据查询平台每次只能查询少量数据且不可导出(复制);二是数据导出工单,数据导出工单允许业务拿到数据。
数据导出工单 这个功能主要是面向研发同学,即数据的操作者。我们可以从下面几个角度分析:
基于上述的合理需求,作为 DBA 应该予以协助完成。有经验的 DBA 都应该知道,这种工作重复性很高又比较繁琐,耗时可能还很长。如果偶尔处理一两个需求尚且能接受,但是随着业务的不断壮大、管理的数据库集群越来越多、数据量越来越大,一个月几十个上百个这种需求也很正常。
我相信,任何一个正常的 DBA 面对这种情况都能疯掉,那么 数据导出工单 功能需求就应运而生了。
当然,这个功能还有一个重要的作用,它可以明确记录每个操作的时间、执行人及数据内容,以便于事后审查与责任划分。
至此,我们把业务的需求明确了,那就可以开始干活了。
下面是一张 DBA 为该功能设计的工单审批流程。
这里只是简单的介绍一下 1.0 时代的工单处理流程,2.0 的部分会详细介绍。
读完请读者思考一下,有没有发现这个处理流程存在的安全问题?
固定审批流存在重大安全问题,不管被导出的数据是否敏感,只要集群负责人的审批即可。其次,业务人员通过这个工单可以将线上的数据全部导出来。
可见当集群负责人权限过大,与其能承担的责任相当不匹配,风险不可控。
形成工单流后,审批人没法看到目标数据的具体情况。比如,该导出的数据量条数,数据是否包含敏感数据等。
这些信息只能通过跟提交人沟通获取。假如工单提交者对实际情况进行隐瞒,审批人是没法发现的。
生成的 URL 未进行鉴权,即只要有查看工单详情的人员都可以通过点击该 URL 进行下载。
数据文件未加密,即下载以后可直接查阅。
以上,梳理分析才发现,这个工单存在的问题真不少。
其实这些问题不是 DBA 发现的,或者说我们在开发之初就没考虑那么全面,默认使用者都是好人,要不然当初也不能设计这样的处理流程。这个 1.0 功能在上线四年后,安全组的同学找到 DBA,希望严格把控线上数据的流出,这才有了上述几点问题的总结。
随后,我们开始了长达一年多的整改,进入 2.0 时代。
下面是一张 2.0 版本的工单审批流程。
可以看到 2.0 新增了数据分类分级判定,这个主要做的事情就是将业务 SQL 语句进行解析,将表名、字段名等信息都拿到,然后判断是否是敏感数据。另外会通过执行计划预估导出的数据行数(预估行)。最终会将这些信息返回到工单详情页,审批人可以看到,可以作为审批参考。
预估行:通过执行计划得到,不准确但可以作为参考。如果 SQL 是全表扫描,这个值跟实际情况可能相差很大。
数据分类分级判定:公司内部系统,会定期扫描线上的表和字段,抽样检查样本数据,最后对表及字段形成敏感等级,如果导出工单涉及到最敏感级别的数据将可能需要 CEO 审批。
要实现这个数据分类分级判定功能,我们还需要解决三个大问题。
终于来到了文章最核心的部分。
要实现 实现数据分类分级判定 功能需要解决三个大问题:
下面详细说明。
大家先来思考一下这个需求好做吗?我的回答是非常难实现,这无异于是实现了 SQL 解析的功能。
MySQL 所有 SQL 语句类别里面,SELECT 一定是最复杂的,所以想实现获取表名、字段名是十分有难度的。
有些同学可能就会说了,有很多开源工具都支持 SQL 解析,直接拿来用就好了。这么说也没问题,但是即便使用了开源工具,还是会存在很多问题,归根结底还是因为 SELECT 语句太复杂。下面就以 Python 代码为例。
Python 的 sqlparse 模块是 SQL 的解析库,但是在使用过程中遇到了很多问题,下面列举一些我遇到的问题:
inner join
、left join
、right join
、join
、,
等,这些会对结果产生影响,可能无法获取预期内的表名列表(没错, FROM 后面的逗号也是一种特殊的连接查询语法)。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"}
至此获取表名,字段名列表的工作就算完成了。
这部分工作倒是没什么特别复杂的,就说几个注意事项:
(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)
多表连接需要多次乘积,比如这里的案例需要
849643 * 849643
,另外,如果这里解析异常可以给一个默认值,比如 10000。
这部分工作每个公司的情况都不一样,且涉及到安全问题就不展开细说了,大致介绍一下实现的逻辑。
敏感等级的判定分两个维度:命名 和 实际存储的数据。
需要定期去扫描业务表,获取到表名、字段名。需要注意的是,跑完全量样本后,后续每次只需要扫描增量即可。
实现增量扫描,需要做到将新建表、删除表、新增字段、删除字段等操作告知扫描系统即可,即:需要有建表/删表/改表工单。比如:
user
的表,会被视为敏感类型。username
、passwd
等字段,也会视为敏感类型。这里也需要定期去扫描业务表,获取到样本数据。需要注意的是,跑完全量样本后,后续每次只需要扫描增量即可。比如:
以上就是《敏感数据分级判定规范》制定的原则和思路,具体的匹配规则需要根据各自公司的实际情况进行设定。
解决上面三个问题后,终于可以完成 2.0 流程中的数据库分类判定步骤了。
数据导出工单 会根据执行计划结果的预估行及数据敏感等级综合评估,然后给一个最终等级。依赖导出数据条数的判断是参考了相关法律法规。
(一)致使泄露行踪轨迹信息、通信内容、征信信息、财产信息五百条以上的;
(二)致使泄露住宿信息、通信记录、健康生理信息、交易信息等其他可能影响人身、财产安全的用户信息五千条以上的;
(三)致使泄露第一项、第二项规定以外的用户信息五万条以上的;
(四)数量虽未达到第一项至第三项规定标准,但是按相应比例折算合计达到有关数量标准的;
数据敏感等级越高,就需要更高级别的领导审批,最高到 CEO。
申请人直属领导 --> 集群负责人 --> 集群的部门领导 --> 安全组 --> 集群的部门领导的直属领导 / CEO
这里需要注意,有可能会出现同一个人出现在不同的审批节点上,针对这种情况需要自动通过。
比如,A 申请了本部门的一个数据导出工单,A 的直属领导是 B,同时 B 又是部门领导,这时候一级审批和三级审批都是 B,B 只需要在一级审批的时候审批,等二级审批后到三级审批需要自动通过(如果判定失败可以给一个默认数据等级并发通知给 DBA)。
数据导出工单 应该说是一把双刃剑,既方便业务也方便 DBA,但是也带来了很多安全问题,我们现在所做的事情就是对其进行收口,尽可能的做到防止未经授权的数据泄露出去,也可以起到责任追溯的作用。
希望该功能在设计和实施中,可以牵引业务人员在使用数据导出功能的同时,符合安全管控规范,有效提升数据防泄漏能力。
改造后,数据导出工单数量在同周期内下降了 57% 且工单撤回率提升 25%。