按「常见程度」排,排在最前面的不是黑客,而是「弱密码、权限太大、误操作、没备份」。
实测一次 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 秒实验
- 把参数化那行的
?改回字符串拼接,但给输入做一次「把'换成''」的转义,再跑同样的输入。 这次返回 0 行,看着防住了——但把输入换成1 OR 1=1(数字型)再试一次。 - 把 MD5 那段里的算法换成
sha256,重测耗时。 还是毫秒以下——说明「快」不是 MD5 独有的问题,所有「一次性普通哈希」都不能拿来存密码。 - 想一想:如果你的库已经被注入了,事后你最需要什么? 审计日志(谁在什么时候查了什么,17.2 讲)——没有它,你连「丢了哪些数据」都说不清。
常见误区
- 「用了 ORM 就不会有注入」。
ORM 也可能被拼字符串(
where("name = '" + x + "'")这种写法照样注入)。 关键不是用什么库,是「值有没有走参数」。 - 「内网系统不用考虑安全」。 内网恰恰是「权限太大、默认密码」最集中的地方,而且一旦有人进来,横向扩散特别快。
- 「密码加个 MD5 就安全了」。 MD5 快到一秒几百万次——加盐只能防「批量预计算」,防不住「针对你一个人暴力猜」。 存密码要用慢哈希(PBKDF2/bcrypt/scrypt,16.4 讲过)。
自测三问(点开对答案)
1. 数据安全要保的三件事是什么? —— **机密性(不被看到)、完整性(不被改坏)、可用性(要用时能用)。** **三者经常互相冲突,所以安全永远是取舍。** 2. 为什么「拼字符串」会被注入,参数化就不会? —— **拼接让外部输入有机会变成 SQL 语法的一部分;参数化把值单独传,**它永远只是值,变不成语法。 3. 按常见程度排,最该先处理的三件事是什么? —— **弱密码(含默认密码)、权限给太大、没备份(或备份没验过)。** **这三件都不是技术难题,是管理问题。**下一篇 17.2 讲技术手段:审计(能证明谁改的)、脱敏(能看业务看不见隐私)、 行级安全(只看自己那几行)——权限和备份之外的那一层。
- ☐ (本篇完,下一篇 17.2 数据安全技术)