🪟 第17章 数据安全
📄 一句话概括

按「常见程度」排,排在最前面的不是黑客,而是「弱密码、权限太大、误操作、没备份」。 实测一次 SQL 注入:拼字符串的查询带上 ' OR '1'='1 直接返回全部 4 个账号。 用实测耗时算破解时间:MD5 存密码试完 1 亿个只要 2.8 分钟,PBKDF2 要 114 天。

📝 本章和前面几章的关系

第 16 章讲「谁能看」(账号、角色、权限),这一章换到更上面一层:整个数据资产的安全。 教材 17.1 分三小节:数据安全的概念、数据安全的内容、威胁从哪儿来。 第三小节是重点——搞清楚敌人在哪,后面 17.2 的「技术」才知道该往哪儿放。 这一节的演示全是真跑的: 注入是真的注进去了,耗时是真的测出来的。 只提醒一句:里面的注入代码是「反面教材」,别抄去生产环境。

一、数据安全到底保三件事

要保什么 被破坏会怎样 靠什么
机密性(不被看到) 客户名单、价格、身份证泄露 权限、加密、脱敏
完整性(不被改坏) 金额被改、记录被删 约束、事务、审计
可用性(要用时能用) 库挂了、数据没了 备份、恢复、冗余

这三条就是安全领域的 CIA 三要素(Confidentiality、Integrity、Availability)。 教材没照抄这个词,但讲的内容就是这三块。 三者经常互相冲突。 加密最安全,但忘了钥匙就等于数据没了(可用性归零); 备份越多越安全,但备份本身也是泄露渠道。 所以「安全」永远是取舍,不是「全都拉满」。

二、威胁从哪儿来:按常见程度重排

威胁 有多常见 后果
弱密码、默认密码 极高 整个库被拖走,而且没人发现
权限给太大 极高 一个人出事,全库遭殃
误操作(删错表、更新漏 WHERE) 高 数据没了,而且不一定是「被攻击」
没备份、备份没验过 高 出事之后只能认
SQL 注入 中 能被拿走任意数据
勒索软件 中(在上升) 文件和备份一起加密
内部人员带走数据 中 查不到、也追不回

这张表是按「常见程度」排的,不是按「技术含量」排的。 真正拖垮系统的,多数不是高手攻破,而是「权限太大 + 密码太弱 + 没备份」这三件事。 所以安全的第一步不是买设备,是把这三件事做完。教材那句话说得对: 安全问题里,管理因素往往比技术因素更致命。 第三行特别值得说: 「更新漏 WHERE」不是攻击,是手滑—— 但它能把整张表的价格改成同一个值,而且没有备份就回不来。 这类事故的频率远高于「被黑客入侵」。

三、SQL 注入:条件是怎么被「加宽」的

假设登录代码是这么拼的(反面教材):

sql = "SELECT id, uname FROM tb_user WHERE uname = '%s' AND upass = '%s'"
      % (uname, upass)

正常输入时没问题。但如果 upass 传进来的是 x' OR '1'='1,拼出来的语句变成:

SELECT id, uname FROM tb_user WHERE uname = 'admin' AND upass = 'x' OR '1'='1'

'1'='1' 恒真,OR 一接,整条 WHERE 被加宽成「所有行」—— 实测直接返回 4 个账号,密码根本没验证。 更狠的是 x'; DROP TABLE tb_user; --。有些驱动支持一次执行多条语句, 一句 -- 把后面的条件全注释掉,表就没了。 换成参数化(占位符)之后,同样的输入什么也拿不到——返回 0 行。 参数化不是「转义引号」,是「把数据当数据」: 那串东西被当成一个普通字符串值, 永远变不成 SQL 语法的一部分。 所以防注入只有一条铁律:任何来自外部的值都走参数,别拼字符串。表名、列名这种拼不了参数的地方, 用白名单校验(只允许出现在你写死的清单里)。

四、用实测耗时算一算:密码能撑多久

存法 单次耗时 试完 1 亿个(8 位纯数字)要多久
MD5 0.00170 ms 2.8 分钟
PBKDF2(10 万轮) 98.28 ms 114 天

同一台机器、同一份数字,只因为「存法」不同,破解时间从几分钟变成几个月。 这说明两件事: 一是哈希算法选错等于没设防; 二是密码复杂度真的有用——上面算的是「8 位纯数字」,加上大小写和符号,组合数直接翻几千倍。 口径要说清: 这算的是「一台机器单线程」的速度,真攻击者会用显卡并行,能快几个数量级。 但「谁更慢」这个结论不变——慢哈希的意义就是把攻击成本抬到不划算。 顺带一句:这张表也解释了「为什么不能限制密码长度」。 长密码比复杂密码更管用(每多一位,组合数乘 26 或 10),而且好记。

五、把威胁分两层,做法完全不同

层次 例子 靠什么防
技术层 注入、弱哈希、越权访问 参数化、慢哈希、最小权限
管理层 权限太大、共用账号、没备份、离职没回收 流程、审计、定期复查

技术层的洞,补一次就补上了;管理层的洞,得靠流程一直盯着。 这也是为什么「安全」不是一个功能,是一个持续的动作。 判断一个团队安全做得好不好,看两件事就够: 一是有没有「定期复查权限」的流程,二是有没有「真的演练过恢复」。 这两件事都跟买什么设备无关。

六、动手 Demo:注入真跑一遍,再算算密码能撑多久

这一段三件事: 把威胁按常见程度列一遍、真跑一次 SQL 注入(拼接 vs 参数化)、拿实测耗时算出「破解要多久」。 注入那段是反面教材——看懂它为什么能成,才知道参数化到底防住了什么。

import sqlite3

c = sqlite3.connect(u"inj.db", isolation_level=None)
c.execute(u"CREATE TABLE tb_user (id INTEGER PRIMARY KEY, uname TEXT, upass TEXT)")
c.executemany(u"INSERT INTO tb_user (uname, upass) VALUES (?, ?)", [
    (u"admin", u"Adm1n@2026"), (u"zhang", u"111111"),
    (u"li", u"222222"), (u"wang", u"333333")])

# 反面教材:拼字符串
bad = u"SELECT id, uname FROM tb_user WHERE uname = '%s' AND upass = '%s'" \
      % (u"admin", u"x' OR '1'='1")
print(len(c.execute(bad).fetchall()))        # 4:全部账号

# 参数化:同样的输入,一条也拿不到
rows = c.execute(u"SELECT id, uname FROM tb_user WHERE uname = ? AND upass = ?",
                  (u"admin", u"x' OR '1'='1")).fetchall()
print(len(rows))                              # 0

实跑输出:

--------------------------------------------------------------------
威胁从哪儿来:先把敌人看清
--------------------------------------------------------------------


  ① 教材列的那几类,按「常见程度」重排
  --------------------------------------------------------------------
| 威胁 | 有多常见 | 后果 |
|---|---|---|
| 弱密码、默认密码 | 极高 | 整个库被拖走,而且没人发现 |
| 权限给太大 | 极高 | 一个人出事,全库遭殃 |
| 误操作(删错表、更新漏 WHERE) | 高 | 数据没了,而且不一定是「被攻击」 |
| 没备份、备份没验过 | 高 | 出事之后只能认 |
| SQL 注入 | 中 | 能被拿走任意数据 |
| 勒索软件 | 中(在上升) | 文件和备份一起加密 |
| 内部人员带走数据 | 中 | 查不到、也追不回 |

  这张表的顺序是「按常见程度」排的,不是按「技术含量」排的。
  真正拖垮系统的,多数不是高手攻破,而是「权限太大 + 密码太弱 + 没备份」这三件事。
  所以安全的第一步不是买设备,是把这三件事做完。
  教材那句话说得对:安全问题里,管理因素往往比技术因素更致命。

  ② SQL 注入:真跑一遍,看它怎么把条件「加宽」
  --------------------------------------------------------------------
  表里有 4 个账号。先看拼字符串的写法(upass 故意写错):

  SELECT id, uname FROM tb_user WHERE uname = 'admin' AND upass = 'x' OR '1'='1'
  返回 4 行:admin、zhang、li、wang
  四条全出来了,密码根本没验证。 因为 '1'='1' 恒真,整条 WHERE 被加宽成「所有行」。
  更狠的是 upass = 'x'; DROP TABLE tb_user; -- —— 有些驱动支持多语句,
  一句注释符 -- 把后面的条件全废掉,表就没了。

  换成参数化(占位符)之后,同样的输入什么也拿不到:

  1> SELECT id, uname FROM tb_user WHERE uname = ? AND upass = ?
  返回 0 行
  参数化不是「转义引号」,是「把数据当数据」: 那串东西被当成一个普通的字符串值,
  永远变不成 SQL 语法的一部分。
  所以防注入只有一条铁律:任何来自外部的值都走参数,别拼字符串。
  表名、列名这种拼不了参数的地方,用白名单校验。

  ③ 用实测耗时算一算:密码能撑多久
  --------------------------------------------------------------------
| 存法 | 单次耗时 | 试完 1 亿个要多久 |
|---|---:|---:|
| MD5 | 0.00170 ms | 2.8 分钟 |
| PBKDF2(10 万轮) | 100.32 ms | 116 天 |

  同一台机器、同一份数字,只因为「存法」不同,破解时间从几分钟变成几个月。
  这说明两件事: 一是哈希算法选错等于没设防;
  二是密码复杂度真的有用——上面算的是「8 位纯数字」,加上大小写和符号,组合数直接翻几千倍。
  注意口径: 这算的是「一台机器单线程」的速度,真攻击者会用显卡并行,快几个数量级。
  但「谁更慢」这个结论不变。

  ④ 把威胁分成两层,做法完全不同
  --------------------------------------------------------------------
| 层次 | 例子 | 靠什么防 |
|---|---|---|
| 技术层 | 注入、弱哈希、越权访问 | 参数化、慢哈希、最小权限 |
| 管理层 | 权限太大、共用账号、没备份、离职没回收 | 流程、审计、定期复查 |

  技术层的洞,补一次就补上了;管理层的洞,得靠流程一直盯着。
  这也是为什么「安全」不是一个功能,是一个持续的动作。

两次查询只差一个写法,结果一个 4 行、一个 0 行。 这就是注入的全部秘密: 拼接让「数据」有机会变成「语法」,参数化把这条路彻底堵死。 注意参数化不是「把引号转义掉」。 转义能挡住 ', 但挡不住「数字型注入」(id = 1 OR 1=1,压根没有引号)—— 只有参数化是真正按「值」传的,跟语法彻底隔离。 第三段那两行耗时是同一台机器现测的: MD5 约 0.0017 毫秒、PBKDF2 约 98 毫秒。 乘上 1 亿个组合,就是 2.8 分钟和 114 天。 这两个数字比任何「请使用强哈希」的告诫都有说服力。 最后提醒一句: 注入只是「威胁清单」里排第五的那一项。 排前四的「弱密码、权限太大、误操作、没备份」,没有一项能靠「写代码小心点」解决—— 它们要的是流程。

30 秒实验

  1. 把参数化那行的 ? 改回字符串拼接,但给输入做一次「把 ' 换成 ''」的转义,再跑同样的输入。 这次返回 0 行,看着防住了——但把输入换成 1 OR 1=1(数字型)再试一次。
  2. 把 MD5 那段里的算法换成 sha256,重测耗时。 还是毫秒以下——说明「快」不是 MD5 独有的问题,所有「一次性普通哈希」都不能拿来存密码。
  3. 想一想:如果你的库已经被注入了,事后你最需要什么? 审计日志(谁在什么时候查了什么,17.2 讲)——没有它,你连「丢了哪些数据」都说不清。

常见误区

  1. 「用了 ORM 就不会有注入」。 ORM 也可能被拼字符串(where("name = '" + x + "'") 这种写法照样注入)。 关键不是用什么库,是「值有没有走参数」。
  2. 「内网系统不用考虑安全」。 内网恰恰是「权限太大、默认密码」最集中的地方,而且一旦有人进来,横向扩散特别快。
  3. 「密码加个 MD5 就安全了」。 MD5 快到一秒几百万次——加盐只能防「批量预计算」,防不住「针对你一个人暴力猜」。 存密码要用慢哈希(PBKDF2/bcrypt/scrypt,16.4 讲过)。
自测三问(点开对答案) 1. 数据安全要保的三件事是什么? —— **机密性(不被看到)、完整性(不被改坏)、可用性(要用时能用)。** **三者经常互相冲突,所以安全永远是取舍。** 2. 为什么「拼字符串」会被注入,参数化就不会? —— **拼接让外部输入有机会变成 SQL 语法的一部分;参数化把值单独传,**它永远只是值,变不成语法。 3. 按常见程度排,最该先处理的三件事是什么? —— **弱密码(含默认密码)、权限给太大、没备份(或备份没验过)。** **这三件都不是技术难题,是管理问题。**

下一篇 17.2 讲技术手段:审计(能证明谁改的)、脱敏(能看业务看不见隐私)、 行级安全(只看自己那几行)——权限和备份之外的那一层。

相关笔记