第十四章:数据库、SQL与我们亲爱的小鲍比·表
你程序的“健忘症”
欢迎回来,旅行者。让我们从一个你无比熟悉的场景开始。
时间是1986年。你正坐在你的 Commodore 64 前面,屏幕上闪烁着温暖的蓝色辉光。你刚刚为你那款划时代的太空射击游戏写完了最后一个功能:高分榜。你花了整整三个小时,用 DATA 语句(还记得它们吗?1)手动输入了一百个虚构的玩家高分,只为让你的游戏看起来更专业。1000 DATA "ZIGGY", 95000... 1010 DATA "RCR", 92000... 完美。你运行程序,一切正常。
就在你准备输入 SAVE "HIGHSCORE.PRG",8 来保存你的杰作时,悲剧发生了。你妈妈在客厅吸尘,她那台拥有工业时代马力的胡佛吸尘器,功率猛地一蹿,跳闸了。或者更糟,她一不小心,用吸尘器猛地撞开了你房间插座上的电源排插。
“啪。”
屏幕瞬间变黑。一片死寂。
你重启了电脑。熟悉的 READY. 提示符出现了。你颤抖着输入 LIST。
什么都没有。
你那一百行 DATA 语句,连同你那三个小时的辛勤劳动,都消失在以太之中了。你刚刚经历的,我的朋友,就是你那个时代最大的敌人:易失性(Volatility)2。
你程序里所有的数据——你的变量,你的 DATA 语句,你的高分榜——都存在于 RAM(随机存取存储器)中。RAM 有一个绝妙的特性:它快得离谱。但它也有一个毁灭性的缺点:它是易失性的。
这里有一个你在21世纪会经常听到的绝妙类比。想象一下:
- RAM(易失性内存) 就像是你的工作台(Workbench)3。当你工作时,你把你需要的所有工具和零件都摊在上面。它超级方便,触手可及,让你能以闪电般的速度工作。但到了晚上,(或者在你的例子里,当电源被拔掉时),工作台必须被清空。第二天你来的时候,它又变得干干净净,什么都不剩2。
- 持久化存储(硬盘/SSD) 就像是你的工具箱(Toolbox)3。把工具放进工具箱里,你需要弯腰,打开盖子,翻找一下,速度慢得多。但是,你可以关上盖子,锁上它,明天再来,甚至明年再来……你的工具依然在那里。
在你的时代,“持久化”是一个需要你手动去做的、有意识的动作(比如 SAVE 命令)。程序默认是“健忘”的。而在你醒来的这个新世界,数据默认是“永生”的。我们不再满足于在断电时幸存下来;我们期望数据能比我们活得还久。
这种对“永生”的执着,意味着我们不能再把数据随随便便地塞进 DATA 语句里了。我们需要一个更严肃的方案。我们需要一个“外接大脑记忆体”1。一个专业的、患有完美主义强迫症的图书管理员,他会把你说的每一个字都用不褪色的墨水,分门别类地记录在案。
欢迎来到数据库(Database)的世界。
“R”计划,和一种“续集”(SEQUEL)语言的诞生
你可能会以为,数据库和它们那令人生畏的语言 SQL,是从某个阴暗的IT部门地下室里凭空冒出来的。事实远比这更具戏剧性。
让我们把时间调回到1970年代。当你们这些先驱还在为 GOTO 是不是“有害的”而争论不休时(见第三章),一位在 IBM 圣何塞研究实验室工作的研究员,埃德加·“泰德”·科德(Edgar "Ted" Codd),正在酝酿一场将彻底改变世界的革命4。
在科德的时代,主流的数据库(比如层次型数据库)存储数据的方式,严重依赖于它的物理结构。数据记录之间就像焊死了一样,通过指针和预定义的路径相互连接。这就像你在第四章“Bug谷”里遇到的那些C语言指针1——数据和它的内存地址被死死地绑在了一起。如果你想用一种新的方式查询数据,你基本上就得重新设计整个数据库。
科德觉得:“这太蠢了。”4。
他提出了一个天才的想法,称之为关系模型(Relational Model)。科德的核心理念是:数据与数据之间的关系,不应该由物理上的指针或嵌套结构来定义,而应该仅仅基于数据自身的值来建立联系4。比如,Students(学生)表和 Grades(成绩)表,可以通过它们共有的 StudentID(学生ID)这个值联系起来。
这在当时是一个石破天惊的想法。正如科德的同事后来说的,他的目标是“让用户能够以更自然的语言水平工作,而不必关心数据存储的细节”4。
这听起来是不是很耳熟?这正是对第四章中那个充斥着手动内存管理和裸指针的“Bug谷”的直接反抗1。科德试图在数据层面上,把程序员从“地址”的暴政中解放出来,让我们只关心“什么”,而不是“在哪里”。
为了证明科德这个激进的理论是可行的,IBM 启动了一个名为“System R”的绝密研究项目5。1973年,一支团队开始着手构建这个“关系型数据库”的原型。团队中有两位年轻的程序员,唐·张伯伦(Don Chamberlin)和雷蒙德·博伊斯(Raymond Boyce),他们接到了一个任务:创造一种“更自然”的语言,好让普通人(非程序员)也能和这个 System R 对话4。
他们开发的成果,最初被命名为 SEQUEL,全称是“结构化英语查询语言”(Structured English Query Language)。这个名字完美地体现了它的目标:它读起来几乎就像普通的英语句子。
然而,就像所有伟大的科技故事一样,这里也有一个关于商标的狗血插曲。当 IBM 准备将这个产品商业化时,他们的法务部门发现,“SEQUEL”这个名字已经被一家英国的航空公司注册了。在一番抓耳挠腮之后,他们极不情愿地把元音字母给扔了,改名为 SQL1。
(旁白:尽管官方发音是“Ess-Queue-Ell”,但半个世界的人——包括它的发明者——至今仍然把它读作“Sequel”(续集),这似乎更符合它作为数据故事“续集”的身份。)
但故事还有更讽刺的转折。IBM,这家发明了关系模型和SQL的公司,在推出商业产品上却慢得像头大象。与此同时,一个名叫拉里·埃里森(Larry Ellison)的精明家伙,读了科德和张伯伦公开发表的论文,敏锐地意识到这是一个金矿。他没有等待,在1977年抢先一步成立了一家叫“关系软件公司”(Relational Software)的小公司,并发布了他们自己的关系型数据库。
这家公司后来改名为……Oracle4。
是的,你没听错。IBM 发明了这一切,而 Oracle 把它卖给了全世界。这就是硅谷。
SQL的禅意:“妈,来杯咖啡!”
好了,历史课结束。现在我们来谈谈为什么 SQL 会让你,一个来自 BASIC 和C语言世界的“恐龙”,感到大脑抽筋。
原因在于:SQL 是一种你从未真正接触过的、完全不同的思考方式。
你所熟知的所有语言——BASIC、汇编、C——都是命令式(Imperative)编程。你就像一个控制狂微观管理者。你必须手把手地告诉计算机如何完成任务的每一步:“把这个字节从内存地址$A000拿出来,把它和#$05相加,如果结果是零,就跳转到子程序$C128……”
而 SQL,是声明式(Declarative)编程的典范。在声明式编程中,你根本不关心如何做。你就像一个坐在沙发上的大老板,你只管声明你想要什么,然后就翘着二郎腿走开了,把“如何实现”的烂摊子留给别人6。
我们来看几个黄金类比,这比任何教科书都管用:
类比一:喝咖啡7
- 命令式 (你的 BASIC/C 思维): 你想喝杯咖啡。你必须编写如下指令:
10 GOSUB 1000(子程序:站起来)20 GOSUB 2000(子程序:走进厨房)30 GOSUB 3000(子程序:打开橱柜)40 FOR I = 1 TO 10: IF CUPS(I).IS_CLEAN THEN LET MY_CUP = CUPS(I): GOTO 70(循环查找一个干净杯子)50 GOSUB 4000(子程序:洗一个脏杯子)60 LET MY_CUP = DIRTY_CUP(1)70 GOSUB 5000(子程序:烧水)80 GOSUB 6000(子程序:冲泡...)
(天啊,这还没算处理“没咖啡粉了”的异常情况...)
- 声明式 (SQL 思维): 你对着厨房喊:“妈,我想要杯咖啡!”7
类比二:去红龙虾餐厅6
- 命令式 (你的思维): 你跳上一辆出租车,开始对司机发号司令:“好的,师傅。从这里出发,在下一个路口左转。然后保持直行,开1.5公里,直到你看到那个巨大的沃尔玛标志。在沃尔玛那里右转,然后数三个红绿灯。在第三个红绿灯……”
- 声明式 (SQL 思维): 你跳上出租车,说:“师傅,带我去红龙虾餐厅。”6
看明白了吗?“妈,来杯咖啡”这个类比的精妙之处在于,它揭示了声明式编程能成功的真正原因:你把繁重的工作外包给了一个隐藏的、智能的执行引擎(在那个例子里,是你妈)。
在 SQL 的世界里,这个“妈”,就是大名鼎鼎的“查询优化器(Query Optimizer)”。
当你(用C#或Java)写一个命令式的 for 循环来查找数据时8:
for (c in collection) {
if (c.equals("uncle bob")) {
return c;
}
}
你是在告诉计算机如何去扫描。
而当你(用SQL)写一个声明式的查询时:
SELECT * FROM collection WHERE name = 'uncle bob';
你只是在说“我想要鲍勃大叔”。
此时,数据库的查询优化器(System R 里的优化器是由 Patricia Selinger 开发的4)就会启动。它会像你妈一样开始盘算:“嗯... collection 表有多大?有10亿行。name 字段上有索引吗?哦,有。那我直接用B树索引(一种高效查找结构)去定位 'uncle bob',这比从头到尾扫描10亿行要快得多。”
如果你非要用命令式去指挥它(“妈!先拿那个红色的杯子!不!水还没开!先放咖啡粉!”),你不仅会惹毛它,而且几乎可以肯定,你这个外行指挥内行的方案,会比它自己优化的路径慢一万倍。
SQL 的美妙之处,就在于放弃控制。你只需要相信,数据库这个“妈”,比你更懂行。
史上最重要的一则网络漫画
好了,你现在被 SQL 的“声明式禅意”给迷住了。你感觉自己像个老板,动动嘴皮子(敲几行 SELECT)就能让数据飞舞。
于是,你用你新学到的知识,开始构建你的第一个现代 Web 应用程序。也许是一个博客,也许是一个论坛,也许是一个让新生入学的表单系统。
我们就拿“新生入学”来举例。你设计了一个简单的网页,上面有一个文本框:“请输入学生姓名”。当用户点击“提交”时,你的服务器端代码(可能是PHP、Python或Node.js)会接收到这个名字,然后把它插入到你的数据库里。
“这太简单了!”你,这位C语言和BASIC的老兵,轻蔑地想。“不就是把一个变量塞进一个字符串里吗?”
所以,你的(比如说)PHP代码可能会这么写,充满了你那个时代的自信和...天真:
$query = "INSERT INTO Students (Name) VALUES ('" + $_POST['student_name'] + "');";
$database->execute($query);
这看起来无懈可击,对吧?我们只是在构建一个字符串,然后把这个字符串发送给数据库执行。它在你的测试中完美运行。你输入“张三”,张三就被插入了。你输入“李四”,李四也被插入了。
有什么能出错呢?
你高高兴兴地上线了你的系统。一切安好。直到某一天,一位姓“表”(Tables)的太太来给她的儿子注册。
几小时后,你接到了学校注册处的电话。
注册处: “你好……请问是技术部吗?我们的学生数据库……好像……呃……没了。”
你: “什么叫没了?!!”
注册处: “就是……所有的学生表都不见了。我们这里出了点电脑故障。哦,是吗?是关于您儿子小罗伯特的吗?”
你: “(一头雾水)……谁?”
注册处: “哦,抱歉,串线了。我们是在给一位新生家长打电话。”
(你听到电话那头传来对话)
注册处: “喂?是表太太吗?是的,关于您儿子的注册……您给您儿子起的名字,是不是叫
Robert'); DROP TABLE Students;--?”家长(表太太): “哦,是的。不过我们都亲切地叫他‘小鲍比·表(Little Bobby Tables)’。”
注册处: “嗯……是这样的。我们把整个学生数据库都给弄丢了。希望您对您做的一切感到满意。”
家长(表太太): “也希望您学会了**净化(Sanitize)**你们的数据库输入。”
这个故事,改编自 XKCD 网站上的一则传奇漫画9,是整个编程界最著名、最重要的安全笑话。它幽默地概括了一种毁灭性的、随处可见的、价值数十亿美元的灾难:SQL 注入(SQL Injection)。
慢镜头拆解:一个笑话如何摧毁一个数据库
你现在正坐在你的办公桌前,冷汗直流。数据库没了。“小鲍比·表”这个名字在你脑中回响。这到底是怎么发生的?
让我们戴上法医手套,对这场灾难进行一次逐帧、逐字符的慢镜头拆解。这是本章的重中之重。
犯罪现场重现:
-
程序员的(天真的)代码模板
你写下的代码,期望着一个“数据信封”,里面只装着“名字”。
INSERT INTO Students (Name) VALUES ('[用户输入]'); -
小鲍比·表的(恶意的)用户输入
这位“黑客妈妈”在网页的文本框里,输入的不是一个名字,而是一串精心构造的指令10。
Robert'); DROP TABLE Students;-- -
当程序(愚蠢地)拼接字符串时,数据库实际收到的命令
你的 PHP 代码忠实地执行了字符串拼接。它把模板和输入拼在一起,形成了一个全新的、灾难性的字符串10:INSERT INTO Students (Name) VALUES ('Robert'); DROP TABLE Students;--');
你的数据库收到了这个字符串。它不像人类那样会觉得“这名字好奇怪”。它是一个服从命令的机器。它开始解析这个字符串。
为了让你,这位“恐龙”朋友看得更清楚,我们来用一张解剖台表格,分解这个“复合型灾难”的每一个组成部分:
| 组件 | 类型 | 在攻击中的毁灭性用途 |
|---|---|---|
Robert |
数据 | 这是伪装。它让这个字符串的开头看起来像一个平平无奇的、无辜的名字。 |
' (单引号) |
武器 (上下文切换键) | 这是整场攻击的**“扳机”**。你,程序员,用一个单引号开始了“数据”部分 (VALUES ('...)。而小鲍比,用他自己的单引号,提前结束了“数据”部分。SQL现在认为:VALUES ('Robert') 这个指令已经写完了。 |
); (括号和分号) |
新指令的序章 | ) 关闭了 VALUES 子句。而分号 (;) 是SQL的“句子结束符”。它用英语来说就是“句号”。它告诉数据库:“很好,第一条指令(INSERT)到此结束。现在,请准备接收我的第二条指令。” |
DROP TABLE Students; |
有效载荷 (炸弹) | 这不是数据!这是一个全新的、语法完美的、毁灭性的第二条指令10。数据库(就是我们那个强迫症图书管理员)收到指令,耸耸肩:“哦,老板(你)又发了一条新指令,叫我‘删除学生表’。好的,马上执行。” 于是,“轰!” |
-- (双减号) |
“清理小组” (注释) | 这是SQL的“注释”符号11。它告诉数据库:“从这里开始,直到这一行的末尾,所有的东西全都当成废话,统统忽略。” 这一步精妙至极,因为它巧妙地让数据库忽略了你原始代码中剩下的、语法不通的“垃圾”——也就是那个多余的 ');。 |
现在你明白了吗?
这根本不是数据库的“Bug”!
数据库完美地执行了它收到的指令。它被告知要执行两条指令,它就忠实地执行了两条10。
这场灾难的根本原因,是一个深刻的、近乎哲学的缺陷,这个缺陷在几十年的软件工程中反复出现:SQL(在那个层面)没有真正区分“控制平面”和“数据平面”12。
换句话说:
- 程序员的意图: 我正在发送 1条指令(INSERT) + 1块数据(学生的名字)。
- 攻击者的利用: 攻击者使用了一个特殊的“元字符”(
') 12,像“越狱”一样,从“数据”的牢房里爬了出来,跳进了“控制室”。 - 数据库的现实: 数据库收到的是一串混合的字符串,它无法(也没被要求)区分哪部分是“你(程序员)的指令”,哪部分是“用户(黑客)的指令”。
你犯的错,就是允许一个匿名的、不怀好意的陌生人(小鲍比的母亲),从一个本该只装“数据”的信封里爬出来,溜进“指令”信封里,然后对你的数据库发号施令。
当小鲍比长大成人:Equifax、Fortnite 和价值数十亿美元的灾难
你可能在擦着冷汗想:“好吧,这只是个有趣的笑话。现在是2024年了,总没人再这么傻了吧?这都是些老掉牙的PHP教程里的问题了。”
你一边这么想着,一边打开了新闻。
大错特错。
“小鲍比·表”不仅没有消失,他长大成人,穿上了西装,拿到了MBA学位,现在专门负责摧毁全球500强企业和政府机构。这个“笑话”在现实世界中一点也不好笑。
- Equifax (2017): 美国三大信用局之一。攻击者利用一个面向公众的网站应用程序中的 SQL 注入漏洞,在系统内部潜伏了数月,最终窃取了1.47亿美国人的机密信息,包括社保号码、生日、地址和驾照号码13。
- Fortnite (2019): 就是你孙子们正在沉迷的那个游戏。一个被发现的 SQLi 漏洞,可能允许攻击者访问超过3.5亿个用户账户的全部信息14。
- MOVEit (2023): 一个被广泛用于企业和政府部门的安全文件传输工具。一个 SQLi 漏洞被黑客组织利用,导致了大规模的数据泄露,受害者包括美国能源部、英国石油公司(BP)、英国航空公司和数不清的其他组织14。
问题来了:这个漏洞已经臭名著了超过17年(甚至更久)15,并且防御它的对策“相对简单”15,为什么它仍然年复一年地出现在 OWASP(开放式Web应用安全项目)的十大安全漏洞列表上?16
答案令人沮丧,因为它与其说是一个技术问题,不如说是一个社会经济问题。
正如一篇研究所揭示的,SQLi 的顽固存在,是几个可悲的人类因素共同作用的结果17:
- “过度劳累的开发人员” 面临 “不合理的最后期限”。
- 当一个疲惫的程序员在周五下午5点需要“赶紧把这个表单功能搞定”时,他会选择最快、最“显而易见”的方法。而“字符串拼接”就是他脑子里跳出来的第一个念头。
- 网上(尤其是早期的)教程和代码示例,为了“简单易懂”,就是用字符串拼接来做演示的。
- 这使得 SQLi 成为了黑客眼中“低垂的果实”(Low-Hanging Fruit)17。
这创造了一个由“图省事”驱动的完美风暴:疲惫的程序员走了最省事的(不安全的)捷径,这反过来又为“脚本小子”(Script Kiddies,指那些只会使用现成黑客工具的低水平攻击者)17 创造了最省事的攻击路径。
高级黑客游戏:“二十个问题”
“好吧好吧,”你妥协了,“我懂了。字符串拼接是魔鬼。但我很聪明!我把我的服务器配置得很好。就算有 SQLi 漏洞,我的服务器也不会显示任何数据库错误信息,更不会把数据吐到网页上。它只会默默地失败。这样黑客就什么也看不到了,对吧?”
你真这么想吗?你太小看小鲍比了。
欢迎来到一个更阴险的游戏:盲注(Blind SQL Injection)18。
顾名思义,在这种攻击中,攻击者是“被蒙上眼睛的”19。他无法直接看到数据库的响应。DROP TABLE 这样的命令可能会失败,或者干脆被阻止。
但他不需要看到数据。他只需要推断出数据就行了。
这个游戏,我们称之为“二十个问题”(20 Questions)19。攻击者不再粗暴地喊“把所有东西都给我!”,而是开始耐心地问一系列“是/否”问题。
攻击场景20:
假设你的网站有一个产品搜索功能。
- 如果你搜索 "masks"(口罩),页面会正常返回一个“口罩”的产品列表。
- 如果你搜索 "asdfghjkl"(一串乱码),页面会返回“未找到结果”。
攻击者现在有了一个完美的“是/否”预言机(Oracle)。“页面正常”=“是”。“页面为空”=“否”。
现在,攻击者开始提问。
提问1(测试是否脆弱):
攻击者在搜索框输入:' AND 1=1
- 数据库收到的查询(简化后):
...WHERE product_name = '' AND 1=1 1=1永远为True。查询成功。- 页面响应: “未找到结果”。(很好,这是我们的“否”基准)
提问2(确认):
攻击者输入:' OR 1=1
- 数据库收到的查询:
...WHERE product_name = '' OR 1=1 1=1永远为True。OR 条件导致查询总是成功,返回所有产品。- 页面响应: 页面正常显示了(可能是所有产品)。
- 啊哈! 攻击者现在确认了:通过观察页面是“正常”还是“空”,他们可以准确地知道他们注入的条件是 True 还是 False。
现在,游戏真正开始了。攻击者要窃取管理员的密码21。
提问3: “管理员表(Admins)的密码,第一个字母是 'a' 吗?”
- 注入:
' AND (SELECT SUBSTRING(password,1,1) FROM Admins) = 'a' - 页面响应: “未找到结果”。(“否”)
提问4: “第一个字母是 'b' 吗?”
- 注入:
' AND (SELECT SUBSTRING(password,1,1) FROM Admins) = 'b' - 页面响应: “未找到结果”。(“否”)
......(这个过程被一个脚本自动执行)......
提问18: “第一个字母是 'p' 吗?”
- 注入:
' AND (SELECT SUBSTRING(password,1,1) FROM Admins) = 'p' - 页面响应: 页面正常显示了! (“是!”)
提问19: “第二个字母是 'a' 吗?”
- 注入:
' AND (SELECT SUBSTRING(password,1,2) FROM Admins) = 'pa' - 页面响应: “未找到结果”。(“否”)
......
这个过程就像在黑暗中玩“猜猜我是谁”,通过成千上万个“是/否”问题19,极其缓慢、耐心、但又无情地,一个字符一个字符地把你的整个数据库给猜出来。
最终防御:给你的SQL一本“疯人填词”(Mad Libs)
你现在彻底崩溃了。“这还怎么玩?!不拼接字符串,有漏洞。配置好服务器,还有‘盲注’。难道我只能拔网线了吗?”
别急。还记得小鲍比他妈在电话里说的最后一句话吗?
“希望您学会了**净化(Sanitize)**你们的数据库输入。”
这是一个陷阱!
bobby-tables.com 网站10——一个专门用来纪念小鲍比的网站——的第一条、也是最重要的一条建议就是:不要试图自己“净化”或“转义”输入!
为什么?因为你这个凡人,不可能比全球的黑客更聪明。你以为你“净化”了单引号 ('),黑客就会用十六进制编码来绕过你。你“净化”了 DROP 关键字,黑客就会用 UPDATE 或者更晦涩的系统函数来攻击你。试图自己“清理”数据,就像在玩一场你注定会输的“打地鼠”游戏22。
你需要一个一劳永逸的、从根本上解决问题的方案。
这个方案,就是参数化查询(Parameterized Queries),在某些圈子里,它更广为人知的名字叫预备语句(Prepared Statements)23。
这个理念极其简单,但也极其强大:从物理上,把“指令”和“数据”彻底分开。
为了让你那颗80年代的大脑理解这个概念,我们来使用一个你可能玩过的游戏:
全新的、完美的类比:“疯人填词” (Mad Libs)
-
字符串拼接(坏 / 小鲍比攻击):
你(程序员)递给你的实习生(数据库)一张白纸和一个打开的信封(里面装着用户输入)。
你对实习生说:“请在这张纸上,工工整整地写下‘将学生 [某某某] 加入数据库’,[某某某]的名字,就在那个信封里,你照着抄就行。”
实习生打开信封,发现里面的纸条写着:“Robert'); 烧掉档案柜;--”。
实习生(一个没有自主判断力的机器人)耸耸肩:“好吧,老板的指令真奇怪。” 他忠实地在纸上写下:“将学生 Robert 加入数据库。” 然后,他看到了下一条指令:“烧掉档案柜。”
于是,档案柜(Students 表)被烧毁了。 -
参数化查询(好 / 疯人填词防御):
你(程序员)这次学聪明了。你递给实习生(数据库)一张预先印好的“疯人填词”表格。
这张表格上,所有的“指令”都已经是印刷体了,无法更改。表格上写着:学生入学表
指令:INSERT INTO Students (Name) VALUES ( [___________________] );
学生姓名:[___________________](请在此处填写)
你把同一个恶意的信封交给他。但这一次,实习生的唯一工作,就是把信封里的全部内容,原封不动地抄到那个唯一的空格里。
最后,表格上写着:
学生姓名:Robert'); DROP TABLE Students;--
实习生把表格归档。档案柜(Students 表)安然无恙。你只是多了一个名字非常“朋克摇滚”的新学生。
看,这就是参数化查询的魔力。
在代码里,它是这样实现的(以 C# 为例):24
第一步(发送“疯人填词”模板):
你首先只给数据库发送“指令模板”,用 @Username 这样的占位符来标记“空格”。
cmd.CommandText = "INSERT INTO (Username) VALUES (@Username)";
第二步(发送“数据信封”):
你分别地、安全地把数据“绑定”到那些占位符上。
cmd.Parameters.AddWithValue("@Username", "Robert'); DROP TABLE Students;--");
为什么这在技术上是无敌的?
这就是那个价值连城的洞察:当你把模板(“疯人填词”表格)发送给数据库进行“预备”或“编译”时,它会解析这个模板,并把它转换成一个可执行的查询计划25。
它已经知道了这个指令的结构。它已经知道 @Username 是一个数据槽,而不是一个指令槽。
当你稍后(哪怕只晚了几微秒)把那个恶意的“数据信封”24 发送过去时,数据库根本不会重新解析整个查询。它只是把你那串恶心的字符串(Robert');...)当作纯粹的文本,一把塞进那个它早就准备好的“数据槽”里。
小鲍比的 DROP TABLE 命令永远不会被执行。它只会被当作一个非常非常奇怪的名字存入数据库。
数据永远只是数据。指令永远只是指令。两者永不越界。
而最美妙的是什么?这种方式实际上更快!26 因为数据库只需要“编译”一次那个“疯人填词”的表格(查询计划),然后就可以用不同的数据(不同的信封)高效地重用它一百万次。
它更安全。它更干净。它还更快。你没有任何理由不用它。
第14章的教训:被害妄想症是一种美德
旅行者,我们的数据之旅暂时告一段落。
我们从一个“健忘”但可信的 DATA 语句开始1,那是一个你了解并信任你的“用户”(通常就是你自己)的纯真年代。
现在,我们进入了一个数据“永生”、但充满敌意的数据库世界。在这里,我们遇到了 SQL,一种强大的、“禅宗”式的声明性语言,它能让你像老板一样“声明”你的需求。
但我们也遇到了小鲍比·表(Little Bobby Tables)。
小鲍比教给我们一个黄金法则,这个法则定义了整个现代软件工程:
永远不要相信用户的输入。1
在你的时代(BASIC),“用户”是你。在今天这个万物互联的世界,“用户”是一个匿名的、全球性的、可能是恶意的、可能只是个脚本小子、也可能是一个背后有国家支持的黑客组织。
所以,欢迎回来,旅行者。在80年代,你是一个建筑师。在2024年,你是一个战地工程师(Combat Engineer),你必须假设你收到的每一块砖(每一个用户输入)都可能是一个伪装的炸弹。
现在,去给你的SQL准备一些“疯人填词”表格吧。“小鲍比·表”正在等你。
引用的著作
-
Volatile vs Non-volatile Memory: Understanding the Differences - Crystal Group, 访问时间为 十月 27, 2025, https://www.crystalrugged.com/knowledge/volatile-memory-vs-non-volatile-memory/ ↩︎ ↩︎
-
What's the difference between Memory and Storage? : r/pcmasterrace - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/pcmasterrace/comments/iddsh4/whats_the_difference_between_memory_and_storage/ ↩︎ ↩︎
-
The relational database - IBM, 访问时间为 十月 27, 2025, https://www.ibm.com/history/relational-database ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
IBM System R - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/IBM_System_R ↩︎
-
Imperative vs Declarative Programming - ui.dev, 访问时间为 十月 27, 2025, https://ui.dev/imperative-vs-declarative-programming ↩︎ ↩︎ ↩︎
-
Explained: Imperative vs Declarative programming - DEV Community, 访问时间为 十月 27, 2025, https://dev.to/siddharthshyniben/explained-imperative-vs-declarative-programming-577g ↩︎ ↩︎
-
Declarative programming vs Imperative programming - Kotlin Discussions, 访问时间为 十月 27, 2025, https://discuss.kotlinlang.org/t/declarative-programming-vs-imperative-programming/8429 ↩︎
-
A History and Evaluation of System R - CMU School of Computer Science, 访问时间为 十月 27, 2025, https://www.cs.cmu.edu/~natassa/courses/15-721/papers/p632-chamberlin.pdf ↩︎
-
bobby-tables.com: A guide to preventing SQL injection, 访问时间为 十月 27, 2025, https://bobby-tables.com/about ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
How does the SQL injection from the "Bobby Tables" XKCD comic work? - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/332365/how-does-the-sql-injection-from-the-bobby-tables-xkcd-comic-work ↩︎
-
SQL Injection - OWASP Foundation, 访问时间为 十月 27, 2025, https://owasp.org/www-community/attacks/SQL_Injection ↩︎ ↩︎
-
7 Types of SQL Injection Attacks & How to Prevent Them? - SentinelOne, 访问时间为 十月 27, 2025, https://www.sentinelone.com/cybersecurity-101/cybersecurity/types-of-sql-injection/ ↩︎
-
What Is SQL Injection and How Does It Work? - Avast, 访问时间为 十月 27, 2025, https://www.avast.com/c-sql-injection ↩︎ ↩︎
-
SQL Injection: The Longest Running Sequel in Programming History - Scholarly Commons, 访问时间为 十月 27, 2025, https://commons.erau.edu/jdfsl/vol12/iss2/10/ ↩︎ ↩︎
-
SQL injection - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/SQL_injection ↩︎
-
SQL Injection History: Still the Most Common Vulnerability - Invicti, 访问时间为 十月 27, 2025, https://www.invicti.com/blog/web-security/sql-injection-vulnerability-history/ ↩︎ ↩︎ ↩︎
-
What is Blind SQL Injection? Tutorial & Examples | Web Security Academy - PortSwigger, 访问时间为 十月 27, 2025, https://portswigger.net/web-security/sql-injection/blind ↩︎
-
Blind SQL Injection Techniques and Mitigation - Infosec Train, 访问时间为 十月 27, 2025, https://www.infosectrain.com/blog/blind-sql-injection-techniques-and-mitigation/ ↩︎ ↩︎ ↩︎
-
Blind SQL Injection: Content & Time-Based Attacks - Qualys Blog, 访问时间为 十月 27, 2025, https://blog.qualys.com/product-tech/2023/02/09/blind-sql-injection-content-based-time-based-approaches ↩︎
-
SQL Injection Cheat Sheet - Invicti, 访问时间为 十月 27, 2025, https://www.invicti.com/blog/web-security/sql-injection-cheat-sheet/ ↩︎
-
Why did SQL injection prevention mechanism evolve into the direction of using parameterized queries?, 访问时间为 十月 27, 2025, https://softwareengineering.stackexchange.com/questions/330850/why-did-sql-injection-prevention-mechanism-evolve-into-the-direction-of-using-pa ↩︎
-
bobby-tables.com: A guide to preventing SQL injection : r/programming - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/programming/comments/9krhc/bobbytablescom_a_guide_to_preventing_sql_injection/ ↩︎
-
Benefits of use parameters instead of concatenation - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/8412776/benefits-of-use-parameters-instead-of-concatenation ↩︎ ↩︎
-
Avoid string concatenation to create queries - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/28473476/avoid-string-concatenation-to-create-queries ↩︎
-
Give me parameterized SQL, or give me death - Coding Horror, 访问时间为 十月 27, 2025, https://blog.codinghorror.com/give-me-parameterized-sql-or-give-me-death/ ↩︎