介绍
一家小型户外用品商店的搜索应仅返回名称完全匹配的已上架商品。查询构造错误让搜索改变了这条规则,普通撇号也会使搜索失败。你将观察真实请求路径,修复一个简短的 Python 函数,同时测试安全性与正常结果。
环境已准备好 Workstation、App Server 和 Shop Web。只需基本终端操作和标签页切换能力。本实验会介绍 SQL、此次修改所需的 Python 语法和编辑器操作。数据是合成的,HTTP 服务与 SQLite 解析则真实运行。
完成一次正常商品搜索
本步骤中,你将使用商店搜索,把结果与数据库中的记录联系起来。你正在帮助一家小型户外用品商店修复搜索。搜索必须只返回名称与输入完全一致的已上架商品。这里所有商品和价格都是合成训练数据。
在实验界面选择 Shop Web 标签页,开始本步骤。
表用行保存记录,用命名的列描述记录。下面是预置的 products 表;listed 为 1 表示公开商品,为 0 表示内部原型。价格使用整数训练货币单位。
| id | name | price | listed |
|---|---|---|---|
| 1 | Trail Mug | 18 | 1 |
| 2 | Camper's Cup | 22 | 1 |
| 3 | Prototype Mug | 99 | 0 |
| 4 | ORANGE Bowl | 12 | 1 |
切换到 Shop Web 标签页,在 Product name 中输入 Trail Mug 并选择 Search。rows 应包含 [1, "Trail Mug", 18],error 应为 null(没有错误)。页面向 Python 应用发送请求;应用向 SQLite 查询匹配记录,再返回结果。页面、应用代码和数据库是请求路径中的不同部分。

查询就是向数据库提出的问题。本地教学跟踪显示 SQLite 执行的 SQL:
SELECT id, name, price FROM products WHERE listed = 1 AND name = 'Trail Mug' ORDER BY id
SELECT 指定返回列,FROM 指定表。WHERE 限制行,AND 要求两个条件同时成立:已上架且名称完全匹配。单引号界定 SQL 文本值。ORDER BY id 保证稳定的记录顺序。页面返回的行不含 listed 列,但查询仍用它筛选记录。
Workstation 终端用于发送请求,App Server 打开应用目录,供后续修复代码。这是同一训练主机上的独立终端会话,并非不同机器。网页展示了这种关系,由实际运行的应用提供服务。
切换到 Workstation 标签页,运行以下命令,发送相同请求:
运行以下命令前,请切换到 Workstation 标签页。
python3 probe.py "Trail Mug"
python3 运行预置的 probe.py 客户端。它唯一的参数是搜索文本。双引号把包含空格的名称保留为一个参数;shell 会移除外层引号。客户端把值安全编码为 HTTP 请求,这是 Web 应用使用的请求格式。客户端中的 127.0.0.1 指本训练主机。
阅读 JSON 输出中的 rows、trace 和 error。JSON 是应用的结构化响应:数组用方括号表示,null 表示没有错误。如果页面无法连接,重新加载 Shop Web;连接错误不同于成功搜索后结果为空。完成检查读取真实请求结果,无需报告。
观察输入如何改变查询
本步骤中,你将在本训练商店观察一次范围受限的 SQL 注入测试。SQL 注入是指输入改变查询结构,而非始终作为文本值。只能在预置实验中使用这些测试。
在实验界面选择 Workstation 标签页,开始本步骤。
切换到 App Server 标签页。用 cat 阅读简短函数,该命令只打印文件,不修改它:
运行以下命令前,请切换到 App Server 标签页。
cat query.py
def search(db, text): 定义函数。db 是已打开的数据库连接;text 是请求传来的值。缩进的行构成函数体。sql = 将一个 Python 字符串赋给变量。双引号界定 Python 字符串,其中的单引号界定 SQL 文本。+ text + 把用户值直接拼接到 SQL 中。db.execute(sql) 要求 SQLite 执行查询;fetchall() 收集结果行;return 将结果返回。
切换到 Workstation 标签页,发送以下精确值,包括两个连字符后的空格:
运行以下命令前,请切换到 Workstation 标签页。
python3 probe.py "' OR 1=1 -- "
外层双引号属于 shell。开头的单引号进入 SQLite,结束拼接查询中的空文本值。OR 允许任一条件成立;1=1 永远为真。-- 开始注释,使原来的结束引号和排序子句被忽略。跟踪现在包含:
SELECT id, name, price FROM products WHERE listed = 1 AND name = '' OR 1=1 -- ' ORDER BY id
你应看到全部四行,包括 [3, "Prototype Mug", 99]。该原型未上架。因此,即使请求只读取数据,也越过了预期业务边界。记住:HTTP 响应成功不代表搜索安全。
再尝试一个含撇号的合法名称:
python3 probe.py "Camper's Cup"
存在漏洞的应用返回 rows: []、空 trace,且 error 包含 near "s": syntax error。SQLite 无法执行格式错误的 SQL,因此没有已完成查询可供跟踪。这是预期失败,客户端仍正常退出,你可以继续。
撇号是合法商品数据。删除它会改变请求的名称;拒绝所有含撇号的输入会拒绝顾客。屏蔽 OR 等单词也会破坏预置的 ORANGE Bowl 搜索。修复必须保留值,同时将它与 SQL 结构分开。
绑定搜索值
本步骤中,你将修复函数,同时保留返回列、精确匹配、上架限制和排序。切换到 App Server 标签页。
在实验界面选择 App Server 标签页,开始本步骤。
参数化查询把 SQL 结构与传入值分开。Python 的 SQLite API 用 ? 标记一个值的位置。将值单独传给 execute;不要给占位符加引号,也不要把值拼进 SQL。例如,db.execute("SELECT ?", (text,)) 把传入文本作为数据返回,不会将内容解释为 SQL。
(text,) 是仅含一个元素的元组,即有序参数集合。结尾逗号使它成为元组;单独的 (text) 只是括号内的原值。execute(sql, (text,)) 的外层括号调用函数,传入两个参数:SQL 模板和值元组。数据库驱动把值绑定到占位符。
在终端编辑器中打开预置文件:
运行以下命令前,请切换到 App Server 标签页。
nano query.py
用方向键移动,用 Backspace/Delete 删除,直接输入文字编辑。替换以 sql = 和 return 开头的两个缩进行,使函数准确如下:
def search(db, text):
sql = "SELECT id, name, price FROM products WHERE listed = 1 AND name = ? ORDER BY id"
return db.execute(sql, (text,)).fetchall()
函数体每行前保留四个空格,def 前不缩进。Python 用缩进划分函数体。按 Ctrl+O 保存,按 Enter 确认 query.py,再按 Ctrl+X 退出。原注释可以保留。
应用在每次请求时读取并编译 query.py。保存即可生效,无需重启。未保存的编辑缓冲区不会生效。拼写或缩进错误会出现在下一次响应的 error 字段。重新打开同一文件,更正、保存并重发请求。
切换到 Workstation 标签页,重试两个值:
运行以下命令前,请切换到 Workstation 标签页。
python3 probe.py "Camper's Cup"
python3 probe.py "' OR 1=1 -- "
真实商品现在返回 [2, "Camper's Cup", 22];特殊测试返回 rows: []。两者都为 error: null。数据库中没有名称恰好等于该特殊文本的商品。
跟踪可能显示 Camper''s Cup,或将整段特殊文本放在引号内。SQLite 跟踪显示的是绑定后展开的 SQL,包括转义后的值,而非原始模板。跟踪中出现值并不意味着 Python 拼接了它们:检查 query.py,查看模板与单独的元组。不要用手工转义替代参数绑定。
保留业务搜索功能
本步骤中,你将同时检查安全性和正常功能。禁用搜索或硬编码空响应或许能隐藏原型,却也使顾客找不到商品。
在实验界面选择 Workstation 标签页,开始本步骤。
切换到 Workstation 标签页,再次发送普通名称:
运行以下命令前,请切换到 Workstation 标签页。
python3 probe.py "Trail Mug"
你应仍收到 [1, "Trail Mug", 18]。再确认包含 OR 的合法单词未被拒绝:
python3 probe.py "ORANGE Bowl"
预期为 [4, "ORANGE Bowl", 12]。按普通名称搜索未上架原型:
python3 probe.py "Prototype Mug"
预期为 rows: [] 和 error: null。绑定阻止注入,而未改变的 listed = 1 限制继续控制公开商品。修改时移除该限制是不安全的。
切回 Shop Web 标签页,输入 Camper's Cup,选择 Search,确认商品行出现。再用 ' OR 1=1 -- 重试,确认结果为空且没有错误。浏览器和客户端使用同一个查询函数。

运行本步骤的完成检查。它发送只读请求,还会在临时内存数据库中,使用额外合成行运行你未改动的函数。这在不编辑真实数据库的情况下,检查精确匹配、标点、上架规则和陌生名称。即使商品名称看似 SQL,只要该名称确实存在且已上架,就仍应当作为普通数据被搜到。
如果检查失败,切回 App Server 标签页,重新打开 query.py,对照修复内容检查占位符、元组逗号、缩进和业务限制。保存后重试。保持数据库、客户端和服务器框架不变;修改记录或返回固定答案并不能修复查询。
总结
你从页面追踪到真实 SQLite 查询,观察了输入如何改变结构,并用占位符与单值元组修复了 Python 调用。你检查了普通名称、撇号、范围受限的测试和原有商品上架规则。订单查询挑战将把同样的分离原则应用于另一类业务对象。


