一个看似人畜无害的换行符,悄无声息地穿透了整个数据系统的权限防线。

开源数据探索工具 Datasette 发布紧急补丁,正式推出 0.65.5 与 1.0a40 两个修复版本,以处置由安全研究员 dpfkdlemtp 报告的权限绕过缺陷。该漏洞在 GitHub 安全通告中编号为 GHSA-h547-rmjf-5m2m,CVSS 3.1 评分达到 7.5,属于无需认证即可远程触发的高危漏洞。

受影响范围覆盖了稳定版 0.65.4 及以下,以及 1.0 alpha 系列中的 1.0a0 至 1.0a39。

关键指标与影响面评估 7.5 CVSS 3.1 评分 高危 / 无需认证远程利用 0.65.5 稳定修复版本 同步修复 1.0a40 分支 1 行 核心代码变更 fullmatch 替换 match

隐蔽的语言暗坑

Datasette 是数据界极为趁手的开源工具,能把本地的 SQLite 数据库直接变成可交互的 Web 页面与 API。为了适应多场景交付,系统内置了一套细粒度的权限体系,允许管理员精确隐藏某张特定数据表。

这次事故的核心,不是常见的 SQL 语法注入,而是一起典型的鉴权与执行解耦。

在底层代码中,Datasette 通过 escape_sqlite() 函数决定一个表名是否需要加上引号包裹。如果表名看起来足够规整,程序就会省去转义环节直接拼入语句。为了校验表名,代码使用了标准的 Python 正则表达式:

re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")

问题恰恰潜伏在末尾的元字符上。在 Python 标准库的正则引擎中,美元符号锚点在配合 match() 使用时,默认允许匹配紧随在字符串末尾的一个换行符。当传入形如 secret\n 这样的表名时,Python 判定它完全符合安全规则,无需强制转义。

鉴权层与数据库引擎的语义错位 构造请求 /secret~0A.json 利用 URL 波浪号编码 传入末尾换行符 鉴权判断 资源名 secret\n 判定无策略直接放行 正则误判安全免转义 SQLite 执行 FROM secret\n 换行符退化为空白符 直接调取受保护表数据

穿透是怎样发生的

真正让逻辑闭环并形成可利用攻击面的,是 Datasette 本身的设计特性。

Datasette 支持波浪号编码,允许客户端在 URL 路径中传递特殊字符。攻击者请求 /data/secret~0A.json 时,系统将 ~0A 解码为换行符。

在这一瞬间,上层与下层对同一个对象的理解彻底撕裂了。

站在 Datasette 鉴权系统的视角,用户请求的资源名叫 secret\n。由于管理员只对 secret 配置了禁止查看的保护规则,鉴权体系认定这个名为 secret\n 的表根本没有任何限制,顺理成章地亮了绿灯。

随后,未经转义的表名被直接拼接进 SQL 查询。落到底层 SQLite 引擎时,数据库对未加引号的标识符宽容度极高,直接把换行符当成了普通空白分隔符截断。

最终,引擎实际上执行了针对 secret 表的查询,并将私有数据行原封不动地返回。

这一穿透途径覆盖面极广,包含了系统的 HTML、JSON、CSV、单行详情、数据过滤、代码片段以及自动补全端点。

鉴权层以为在放行不存在的幻影,执行层却精准取出了被锁死的数据。

许多运维者往往依赖兜底策略,例如关闭任意 SQL 查询功能。但在这种场景下,关闭任意 SQL 查询完全失效,因为所有的请求走的都是标准数据读取管线。

  • 风险.只要实例依赖表级权限做数据隔离,攻击者便可通过编码换行符穿透整张私有表。

止血方案与认知偏差

官方的最终修复异常干脆,核心改动只有一行:将校验处的 _boring_keyword_re.match(s) 替换为 _boring_keyword_re.fullmatch(s)

这一改动从源头掐断了尾随换行符通过校验的可能性。一旦表名带有换行,便会被强制打上引号,作为独立的特殊标识符交给 SQLite 解析,从而杜绝了向合法表名退化的语义歧义。

对于无法立即升级的生产环境,官方给出的临时缓解措施是直接配置数据库级别的全局权限,禁止任何非受信用户访问整个数据库文件。单纯依托表级权限构建的护城河,在补丁打上之前已经失去了屏障作用。

排查是否遭遇过定向探测,需审查访问日志中是否存在带有 ~0A 或编码换行符的 HTTP 请求路径。

  • 建议.全面自查基于正则末尾锚点的标识符校验逻辑,改用语义严格的匹配函数。

软件工程里最棘手的安全隐患,往往不在于某一段代码写得过于粗糙,而在于几个成熟模块组合在一起时产生的默契偏差。Python 的历史正则约定碰上 SQLite 宽容的语法容错,看似各司其职,却在接缝处裂开了一道能漏出全部数据的缝隙。