漏洞

一张报表递到最后,列名却变成了 Python——DIRAC CVE-2026-45579 源码复盘

CVE-2026-45579 让一个经过认证的报表字段在 SQL 生成以前进入 Python eval() ,原本狭窄的 RequestManager 字段选择由此变成在 DIRAC 服务账号下执行代码的入口。

暖纸手绘的科学计算运维室里,一张报表卡依次经过服务柜台走向解释器机械,修复后的另一条路则停在两座排列整齐的模型档案柜前。
文章导航

研究依据复核 DIRAC GHSA-9jpv-c7p4-997x、8.0.79、9.0.22 与 9.1.10 固定版本源码、RequestManager/RequestDB 查询路径及其回归测试

来源DIRAC 安全公告与 GitHub Advisory Database / DIRAC 修复源码与版本标签 / Python 与 SQLAlchemy 文档 / SOSEC 一手资料复核

1 夜班从一个普通报表字段开始

1.1 一项分组选择走到了界面从未承诺的地方

一个看起来只想让 DIRAC 按某列分组的已认证报表请求,会在受影响 ORM 表达式形成时抵达 Python 求值器,并早于 SQL 生成,因此 CVE-2026-45579 让低权限调用者能够从报表字段跨进 RequestManager 服务账号正在运行的代码。

夜班运维人员打开 Request Management 报表,只想知道有多少排队记录处于同一状态。页面给出的都是管理员每天会见到的控件:选择分组字段,用过滤条件缩小记录,再决定结果顺序。整个动作不像编程终端,更像柜台人员把三张小卡片递给报表服务;危险恰恰藏在这份日常感里。

第一张卡片可以写着 Status,第二张要求只看某种 Operation Type,第三张说明 Request 编号采用升序。这些词在产品里都只有克制的含义:一个已知列、一项比较值,以及两种排序方向之一。稳妥的实现完全可以把它们解析成 ORM 对象,无需给调用者任何操纵 Python 语法的能力。

到了 DIRAC 9.1.9 的最后一张桌子,事情突然变大。RequestDB 的多处辅助函数把外部字符串拼进表达式,再交给 eval()。Python 解释器先在服务模块环境里处理表达式;只有求值正常返回时,所得对象才会参与形成受影响的 ORM 表达式,而求值期间发生的 Python 副作用已经独立完成。数据库层因此直到危险决定以后才进入现场。

这个先后顺序说明它是 eval 注入,不是通常所说的数据库注入。为 SQL 转义引号,或把比较值做成绑定参数,都保护不了已经被 Python 解释过的代码。关键问题不在最终 SQL 有没有占位符,而在 SQL 形成以前,究竟是谁决定哪个 Python 对象会成为分组、过滤或排序表达式。

这张卡片随后穿过公开公告、接收字符串的处理器和 RequestDB 的八处 Web 查询求值点,最终在 v9.1.10 里被拆成两个已知模型、映射列元数据与两种明确排序选择。源码解释它为什么危险,回归实验说明请求应停在哪里,逐实例发布和历史调查则回答修复怎样真正落到运行环境。

1.2 公开记录证明了严重缺陷,却没有替任何现场写好入侵结论

DIRAC 在 2026 年 7 月 13 日 14:56 UTC 发布 GHSA-9jpv-c7p4-997x;该记录关联 CVE-2026-45579。官方记录把问题定为严重,CVSS 3.1 得分 9.9,归入 CWE-95,即动态求值代码中的指令未得到正确中和。向量描述了一条可经网络到达、复杂度低、需要低权限、不要求用户交互的路径,影响范围发生变化,机密性、完整性和可用性均为高。

需要认证这个前置条件必须写清楚,却不能拿它把问题说小。公告指出,任何能调用受影响函数的已认证用户,都可能以运行 DIRAC 服务的系统身份执行代码或命令。认证把最初人群从整个互联网缩小到部署接受的身份,但科学计算平台往往服务跨机构社区、自动化任务和联邦账号;服务身份即使不是 root,也可能掌握配置、数据库访问、委派凭证或重要网络通道。

GitHub 全局公告把受影响版本写成三段互不连续的约束:>= 6, < 8.0.79>= 8.1.0a1, < 9.0.22>= 9.1.0, < 9.1.10;对应首个修复版本分别为 8.0.79、9.0.22 与 9.1.10。这三段约束共同构成公告发布的受影响集合;已经停止维护的旧分支则应迁移到仍受支持的修复版本。

公告描述了 DIRAC 系统可能遭到全面控制的影响,并举出服务进程可能接触的材料,包括本地配置、数据库密码、已存储的代理凭证与令牌。这是上游对潜在后果的说明,不代表每个部署都把相同材料放在同一位置,也不证明每个服务账号都能读取它们,更不能证明某个站点已经遭到利用。进程实际拥有的权限和现场是否出现过异常,只能靠本地调查补齐。

四类证据分别回答四种问题:公告给出严重度、触发条件、受影响版本和潜在后果;官方标签引用说明这些发布名称在复核时解析到哪些内容寻址的提交对象;v9.1.9 到 v9.1.10 的源码比较说明八处求值怎样消失、由哪些辅助函数接替;上游测试则揭示维护者真正要守住的行为。

这一区分会贯穿全文。固定版本的源码足以证明旧方法会对调用者控制的报表字符串求值;官方测试公开使用了不会执行动作的继承属性 __class__,因此可以说明它怎样被拒绝。截至 2026 年 7 月 18 日,本次公开资料检索未发现已利用或现场事件报道;这不等于证明没有事件。没有检查某个部署,也就不能替它虚构进程可读的秘密、出站路径或日志保留周期。

精确记录标签解析结果,还能防止结论随引用移动而漂移。复核时,官方轻量标签引用的解析结果如下:v8.0.79 对应 a05a64be9fbf6119080b735ae0a4b2d13cf67a48,v9.0.22 对应 8f2dcddb4310790da01299da9cad895423b91669,v9.1.9 对应 03de43326b8078cef21ed6405bccebefc003f745,v9.1.10 对应 2f5c5f3b74ab65b3b7d5687d561bc8deee1e4859。这些引用本身可以移动,所以完整提交号构成本次复核锚点;标签名与短前缀只用于定位,部署证据还要保存实际构建产物摘要。

证据对象能够证明什么单独使用时不能证明什么
GHSA-9jpv-c7p4-997x需要认证、CVSS 9.9、受影响版本、修复版本与潜在后果某个部署是否可达、服务实际持有哪些凭证材料、或是否发生过利用
官方标签引用解析到的提交四项引用在复核时解析到的内容寻址提交身份引用后来是否移动,或运行中的工作进程是否加载该提交
RequestDB 比较八处被移除的 Web 查询求值及其替代辅助函数运营现场的历史请求或进程行为
上游回归测试受支持报表仍正常,继承属性 __class__ 在三条路径都被拒绝未经单独测试的本地扩展、厂商回移补丁或新旧版本并存的实例群

1.4 仪表盘背后,RMS 层级保存 Request、Operation 与 File

DIRAC 的 Request Management System 代表用户异步执行一系列相对简单的操作。官方文档列出故障恢复、数据管理和其他可扩展任务。一条 Request 可以容纳按顺序执行的 Operation,一项 Operation 又能携带 File;状态沿着这组层级传播,使长时间运行的分布式工作在最初客户端离开以后仍能继续。

中央的 ReqDB 保存 Request、Operation 与 File 记录,相应的 Python 类暴露 ORM 映射属性和程序行为。ReqManager 通过 DIRAC 的 DISET 服务协议向 ReqClient 提供查询;Web 门户无需直连数据库,就能取得明细、分组计数和去重后的候选值。三项受影响辅助函数只把这套报表词汇解析为 Request 或 Operation 列;File 仍属于 RMS 存储层级和回归夹具。服务方法因此必须把这套双模型外部词汇可靠地翻译成内部查询。

项目文档的配置示例把中央 RequestManager 放在请求处理代理附近,并将服务授权默认值设为 authenticated;安全公告对这项缺陷也给出了同样的调用者条件。某个部署可能进一步收紧授权,但源码复核不能擅自假设存在本地覆盖。判断暴露面时必须读取实际配置和身份映射,不能把示例误当成所有站点都在放行,也不能反过来假定所有站点都已限制。

修复涉及三项面向 Web 的数据库操作:getRequestSummaryWeb() 返回经过过滤和排序的记录;getRequestCountersWeb() 分组计数;getDistinctValues() 为门户提供去重后的候选值。它们表面上都只读数据,可负责选择列的字符串依旧进入了一个能力远大于只读表单的服务进程。

服务进程与数据库处理的是两种不同语言。SQLAlchemy 负责把映射列、比较条件、连接、分组和排序对象组成 SQL;Python 则能在当前命名空间里解析名称、属性和函数调用。RequestDB 只为取得一个 SQLAlchemy 列对象,却把字符串交给 Python 求值,相当于在两种语言之间架起一座足以容纳通用 Python 语义的桥。报表业务明明只需要从几列中作选择,最终却得到了一台解释器。

系统结构已经画清,下一步就跟着那张卡片从 ReqManagerHandler 进入 RequestDB。处理器的类型声明准确,却不完整:它能保证分组选择是字符串、过滤条件是字典,却没有说明哪些字符串和键名属于产品允许的词汇。正是在这段缺失的约束里,普通报表请求开始改变含义。

ReqProxy 虽然位于相邻拓扑,却不是本补丁修复的代码。DIRAC 文档将它定义为中央 ReqManager 不可用时接收新 Request 的故障转移服务:任务先序列化到本地,稍后再转发。本 CVE 涉及的 Web 报表辅助函数则位于中央管理器的 RequestDB。资产清单仍应纳入 ReqProxy,因为网络拓扑、所用凭证和恢复镜像会影响调查范围;但不能据此暗示每个代理方法都含有这八处求值。

这些记录承载的是运营任务,不是静止的分析数据。Request 中的 Operation 能够异步执行复制、注册或删除数据等动作,并携带所属者信息;受影响的 Web 方法只负责读取和汇总状态,不会直接执行队列中的 Operation。代码求值之所以危险,是因为它跳出了报表原本的只读职责,并继承了服务进程的权限;并不是说分组计数查询本来就被设计成修改 Request。

这套层级还决定了报表查询必须知道自己正在使用哪张表。受影响 Web 路径中的 Request 状态与 Operation 类型来自不同映射对象,Operation 连接也会改变行数和分组结果。File 路径属于更宽的 RMS 层级,但在这里仅作为夹具数据,并非当前处理器或 _get_column() 接受的字段模型。计数辅助函数若未先确定 Request 或 Operation 模型,就可能把合法名称落到错误表上;若忽略连接后的基数变化,又可能给出看似可信的重复计数。因此,修复既要把字段解析限制在预期的 Request 或 Operation 模型内,也要用合成层级记录逐项验证原有的连接、分组和去重语义。否则,即使拒绝了危险输入,报表数字也可能已经失真。

暖纸手绘的证据桌上放着一份公告案卷、三卷发布胶片和成对源码页面,远处未接触的观测站隔在玻璃后。
图 1|每类来源回答不同问题:公告定义严重度与范围,标签引用的解析结果确定本次复核的提交对象,源码比较解释变化,而玻璃后的远端观测站标记本次复核没有测试的现场。

2 字符串穿过服务接口时,仍然穿着普通类型的外衣

2.1 处理器只确认参数外形,没有限定报表词汇

在 v9.1.9 中,ReqManagerHandler 公开 export_getRequestSummaryWeb()export_getDistinctValuesWeb()export_getRequestCountersWeb()。类型声明要求明细查询接收字典、列表和整数等组合,计数查询接收字符串与字典。这能阻止服务协议收到完全不同的外层对象,却分不清一条 Python 字符串装的是合法列名、未知名称,还是一段完整表达式。

计数接口是最短、也最容易看懂的入口。它接收 groupingAttributeselectDict;文档说明前者应是 Request 表字段或特殊名称 Type,随后将两个参数直接传给 RequestDB.getRequestCountersWeb()。ReqManagerHandler 没有维护获准的分组字段清单,而是相信 RequestDB 会正确理解产品词汇。

去重候选值接口做了一小段路由:请求属性为 Type 时选择 Operation 表,其余情况选择 Request,再调用 getDistinctValues(tableName, attribute)。这个分支确实表达了一项产品规则,但余下的属性名仍未验证。选中已知表只能说明对象类别,不能证明名称属于该模型的 ORM 映射列。

明细接口把 selectDictsortListstartItemmaxItems 交给 RequestDB,并在文档中说明特殊日期键和 Operation 类型别名。门户需要组合多种报表,这种灵活性有真实用途;它需要一套能识别受支持字段、数据值和排序方向的解析规则,让未知选择得到普通服务错误,通用表达式不应进入这条路径。

授权与输入语法解决的是两件事。DISET 规则决定谁能调用方法,类型声明决定序列化后的外层值是不是字符串;两者都不会告诉 RequestDB,Status 是 Request 的映射属性,或者 ASC 是受支持的方向。缺陷得以延续,是因为身份和外形检查结束后,RequestDB 仍把字符串当成 Python 语法。

因此服务入口需要两层验收。第一层仍由 DISET 确认参数是字符串、字典、列表或日期;第二层由最靠近数据模型的代码验证词汇和关系,例如表名只能来自少量产品模型,字段必须属于映射器登记的列,排序方向只能命中两个固定选项。把第二层塞进传输协议,会让协议定义与数据库模型紧耦合;完全省略,又会让任何类型正确的字符串一路通行。合适的位置就是 RequestDB 的解析入口:在形成受影响 ORM 表达式时拒绝非法值,并保证对应 SQL 语句不会生成或发出;错误以一致结构返回给 ReqManagerHandler,也不向页面泄露内部类名、SQL 或调用栈。

2.2 明细报表的过滤和排序在数据库柜台拼出了 Python

getRequestSummaryWeb() 开头做的都是正常查询工作:选择 Request 字段、处理分页,并在按 Type 过滤时连接 Operation;日期上下限直接与 Request._LastUpdate 比较。危险的转折出现在通用分支,代码把表名和外部键名插进一条字符串,企图用它取得模型属性。

列表值分支把所选表、键名、in_ 方法和列表的文本表示拼成表达式,再整体求值;标量分支先对“表名加键名”的表达式求值,随后把结果与数据值比较。列表形式尤其能说明问题:列的选择和数据的文本表示一起进入了 Python 语法,尽管 SQLAlchemy 本来就提供类型化的 column.in_(values)

排序又为明细报表增加了第三处求值。第一组排序对给出 Request 属性和方向,代码把两者格式化为“属性加方法”的表达式,将方向转成小写后交给解释器。业务明明只有两个受支持方向,却把对象和方法的选择都委托给了通用 Python。后来的修复会拆开这两项决定,只接受 ascdesc

查询执行后的错误处理无法让这次转换倒流。畸形对象稍后可能被 SQLAlchemy 拒绝,方法也可能返回 S_ERROR,可 Python 已经完成字符串解析与求值。安全测试必须证明非法字段在对应 SQL 语句生成和发出以前被拒绝,不能只因 DISET 最后返回错误,就认定解释器从未运行。

上游公开用例只断言 OK 为 false,并要求消息准确等于“Unknown Request attribute '__class__'”;固定源码的控制流则显示,解析异常会在对应 SQL 执行以前抛出。两项材料足以定位拒绝顺序,却不能替某个部署提供驱动层的零发送计数。要把“没有 SQL 发出”写成当地实测,运营方仍需在隔离回归中增加语句编译与发送计数器,并把结果绑定到准确构建产物和工作进程。

明细报表的排序结构需要单独做兼容测试。处理器接收列表,RequestDB 使用第一组二元值作为列名和方向;测试应覆盖空列表、一个合法排序对、按现有产品行为处理的额外排序对、大小写混合的 ASC/DESC、未知列和未知方向。目标是保留门户实际发送的接口格式,同时让不受支持的选择在解析处失败;回移安全补丁时不能顺手发明新语义。

列表过滤也要准备细致的测试数据。普通字符串可以包含引号、逗号、Unicode 字符和空值,用它们证明 SQLAlchemy 收到的仍是完整数据对象;测试记录编译后的查询结构或绑定参数数量,不依赖随数据库驱动变化的文本格式。这样既能证明移除字符串求值不会破坏合法数据,也能抓住下游分支因复杂列表序列化失败而重新引入字符串拼接的回归。

2.3 计数与去重查询又重复了五次同一决定

计数方法把相同模式扩展到分组和过滤。它先把 Type 翻成 Operation 属性,把 Status 翻成 Request 的私有映射名称,其余选择则变成带 Request 前缀的字符串;随后在构造选中列时对分组表达式求值,又在 group_by() 中求值一次。同一个外部选择因此两次进入解释器。

通用计数过滤还会增加一次求值:列表形式执行动态构造的 in_ 表达式,标量形式则先对模型属性求值再比较。特殊日期上下限继续使用直接的 SQLAlchemy 比较,Type 也会触发必要的表连接。安全的显式分支和不安全的兜底分支混在一起,恰好解释了普通测试为何长期通过:熟悉的报表键名得到预期结果,兜底代码却一直保留着大得多的语言。

getDistinctValues() 是 v9.1.9 RequestDB 中第八处 Web 查询求值。ReqManagerHandler 选择 Request 或 Operation,数据库方法规范化 Status,随后在 distinct() 内把“表名加列名”的字符串交给 eval()。门户也许只用它填充下拉选项,可它抵达的解释器与更醒目的计数路径完全相同。

有了这份清单,修复目标也随之改变。“删掉被标出的 eval”远远不够。真正要保证的是:每一个 Web 报表列、过滤键和排序方向,都必须先从封闭词汇中解析成对象,再交给 SQLAlchemy。测试必须同时覆盖明细、计数和去重候选值,因为它们是三个不同的公开入口,即使修复后共用同一组辅助函数。

去重候选值调用往往发生在人真正打开最终报表以前,因为门户需要先取回下拉选项;如果只围绕“运行报表”按钮采样请求,很容易漏掉它。回归记录要覆盖页面加载和自动补全流量,确认由哪个已认证的后端身份发出,并把这类调用纳入历史检索。一个看似只读的便利接口,也可能沿着主报表提交的同一条路线抵达解释器。

Web 报表路径外部选择v9.1.9 求值点v9.1.10 替代方式安全负向检查
getRequestSummaryWeb()过滤键、列表或标量值、排序列与方向3 处——列表过滤、标量列选择、排序表达式_apply_web_filter()_get_order_expression()继承属性和非法方向在对应 SQL 发出前返回 S_ERROR
getRequestCountersWeb()分组字段与过滤键4 处——分组列选择、列表过滤、标量列选择、分组表达式一次解析 groupingColumn,再使用 _apply_web_filter()继承属性作为分组名时返回“未知 Request 属性”
getDistinctValues()处理器选择表类别,外部输入选择列名1 处——去重列表达式distinct(_get_column(...))继承的 Request 属性在公开路径被拒绝;未知表由辅助函数或回移测试验证
合计三类相关报表语法8 处相关调用共享映射列检查合法行为和三个入口的拒绝行为一起测试

“八处”是一个可以复算的源码范围,不是对整个 DIRAC 仓库里所有 eval() 的总数。复核以 v9.1.9 的 RequestDB 为对象,只统计三个具名 Web 报表方法中参与列、过滤或排序表达式形成的调用:明细三处、计数四处、去重一处。计数分组之所以记两处,是因为同一个外部选择分别在查询选择项和 group_by() 中实际求值;到了 v9.1.10,这两个位置复用同一个已经解析的 groupingColumn。回移补丁的验收也应按方法与语义位置重做这张清单,不能只凭一次仓库级关键词扫描。

计数接口还带来一个容易被严重度掩盖的正确性问题:分组列只应解析一次,并在同一查询里同时用于选择和聚合;去重候选值也必须先确定表与列,再构造去重表达式。若每个片段各自解释字符串,模型选择、别名转换和错误处理就可能漂移。修复后的单一辅助函数把这些决定收束到一处,调用方拿到的是已经验证的列对象。回归时要同时检查合法分组数量、空结果、列表过滤、标量过滤和未知字段拒绝,既确认拒绝前没有发出对应 SQL,也确认合法查询没有因安全改造丢掉原有结果。

2.4 逐字段交接记录,标出普通数据在哪一刻变成程序文本

给请求中的每一项材料分别注明负责人和每个阶段的预期类型,处理器到数据库的交接就容易审计得多。在受影响的 v9.1.9 对象中,ReqManagerHandler 第 229–270 行定义了三个服务入口。类型声明并非虚设:它能拒绝外层 Python 容器类型不符的值,也让序列化过程保持可预期。问题更具体——检查结束以后,某些字符串仍能在 RequestDB 中从产品数据改变类别,成为源码。有效的复核记录因此要同时指出,最后一个仍把值视为数据的组件是谁,第一个赋予它可执行语法的组件又是谁。

明细请求携带四种意义完全不同的对象:selectDict 把外部字段别名映射到标量或列表数据;sortList 携带字段与方向;startItemmaxItems 则控制查询返回后的切片。旧版通用过滤把键名插入模型属性表达式;列表分支还会把整份列表的文本表示插进表达式,标量分支则先求值取得属性,再与仍保持独立的数据比较。排序同时把字段与转成小写的方向塞进方法调用表达式。分页没有参与八处求值,但仍属于资源与兼容测试,因为修复后的查询若改变记录集合或顺序,相同切片也会返回不同内容。

计数请求结构不同,需要拥有独立交接行。groupingAttribute 先经过产品别名转换:Type 变成 Operation 表达式,Status 变成 Request 的私有映射名,其余字符串加上 Request 前缀;所得字符串第一次作为查询选择项求值,又在 group_by() 中第二次求值。与此同时,selectDict 的键值重复通用列表或标量过滤行为。同一项用户选择明明只准备指向一列,却在两个程序位置被解释。因此,只删掉第一处醒目调用的补丁并不完整;固定方法必须先解析一个 groupingColumn,再让两个位置复用同一对象。

去重请求看起来更小,却包含一项重要的归属差异。调用者提供一个属性字符串;ReqManagerHandler 在服务内部选择表类别,Type 对应 Operation,其余属性对应 Request;旧 RequestDB 随后用两个字符串形成交给 distinct() 的对象。正常公开路径上的表选择属于服务,列选择仍受外部影响。这会改变测试重点:“未知表”用于验证辅助函数和下游回移补丁,公开负向用例则应沿正常路由提交未知 Request 或 Operation 列。把内部表变量误写成 Web 直接参数,会夸大接口;忽略外部列名,则会漏掉真实路径。

一张紧凑的交接矩阵可以设置五列:序列化位置、业务角色、决定者、允许语法和类型化输出。例如,明细过滤键位于字典键名,作用是选列,由已认证调用者决定;它应属于 Request 别名集合或有意设计的 Operation Type 特例,输出必须是一个映射 SQLAlchemy 属性。对应数据位于字典值,作用是参加比较,同样由调用者提供;它应符合字段的数据类型与大小策略,并作为相等或 in_() 表达式里的数据保留下来。排序方向既不是列也不是数据,只能变成两项由服务掌握的操作之一。写出这张矩阵,未来重构就不容易对三类完全不同的输入套用同一个“清洗字符串”函数。

矩阵还会显出映射列成员检查没有回答的授权问题。一列在结构上可以安全选择,不代表每个已认证身份都适合读取它。Request 的所属者、错误文本等字段可能包含比广泛门户用户所需更多的运营细节,本地模型也可能增加站点专属数据。补丁中的 column_attrs 只回答名称是否属于两个模型之一的映射列,没有宣称每个映射列都对每个调用者或入口开放。部署方应把结构解析结果,与按照身份、方法和用途制定的字段策略求交集。这是 CVE 修复以外的另一项控制,分开表述才不会错误扩大上游补丁的承诺。

错误也需要交接记录。旧方法可能在 Python 解析或求值、ORM 表达式构造、SQL 生成或数据库执行阶段失败,宽泛异常处理又可能把这些阶段压成相似的服务错误。固定解析器会针对未知表、列或方向有意抛出 ValueError,再由三个公开方法转成 S_ERROR。日志应把阶段记成结构化决定,同时避免复制整段未经信任的值。这样,复核者能确认“Unknown Request attribute”表示名称停在映射器成员检查,而数据库异常属于后续合法查询;一个笼统错误计数器无法提供这种时间证据。

门户结构还可能模糊真正作出选择的身份。人通过浏览器点击报表控件,共享门户后端却可能使用自己的证书或服务令牌向 RequestManager 认证;此时 DISET 看到的是后端主体,未必是选择字段的人。暴露面清单必须同时保存两种身份和关联记录。只把 RequestManager 授权收紧到共享后端,如果门户自身没有同步执行用户与字段策略,并不会减少哪些门户用户能够抵达方法。反过来,直接使用 ReqClient 的调用者可能携带个人凭证,绕过从网页推导出的假设。逐字段交接记录会跟随材料经过每一跳,并注明该处真正获准的主体。

批处理、重试和自动补全还会增加一层运营细节。页面可能先查询去重候选值,再提交明细;门户可能让失败请求在另一只工作进程重试;定时报表也能在没有交互用户时发出相同选择。关联编号要穿过这些转换,最终服务还要附上进程身份和实际版本。新旧版本并存时,两份相同请求因此可能走不同代码。结案需要的证据不能只写“门户返回了错误”,而应写成“这个调用者的这份请求抵达这只工作进程,在这个解析决定处停止,也没有产生对应下游语句”。交接关系已经明确,下一章便能按解释器时序观察事件,不必从最终响应倒猜。

手绘门户柜台把分组、过滤与排序卡片交给中央服务桌,再送到 Request 和 Operation 查询托盘;独立的 File 托盘只作为 RMS 层级与测试夹具背景留在档案柜中。
图 2|门户并不直接访问 ReqDB:带类型的服务调用先经过 ReqManagerHandler,再由三项受影响报表辅助函数只把外部字段选择翻译为 Request 或 Operation 查询。File 托盘展示的是更宽的 RMS 层级与回归夹具,并非当前处理器或 _get_column() 允许的查询模型。

3 决定性事件发生时,SQL 语句还不存在

3.1 eval() 先吃掉语法,SQLAlchemy 才收到对象

Python 自己的文档写得异常直接:eval() 会执行任意代码,把不可信用户输入交给它就会造成安全漏洞。该函数先把源码解析成 Python 表达式,再使用全局和局部命名空间求值;如果像旧版 RequestDB 那样没有显式传入命名空间,求值就发生在调用函数所在的环境。

这一定义锁定了事件顺序。第一步,外部报表字符串变成 Python 源码;第二步,Python 解析名称、遍历属性,并执行表达式允许的动作;第三步,返回对象才会交给 session.query()filter()order_by()group_by()distinct()。Python 完成前一阶段以前,SQLAlchemy 根本还没收到可供绑定、引用或拒绝的字段对象。

预编译语句保护的是另一个阶段:数据库驱动发送语句和参数时,将比较值与 SQL 语法分开。受影响的列表过滤甚至把整组数据的文本表示嵌进 Python 字符串,随后才轮到 SQLAlchemy。一个团队完全可能在其他地方把数据库参数化做得很好,却仍保留这项解释器缺陷,因为多出来的语言是 Python,不是 SQL。

数据库权限依然会影响后果,却无法把求值器限制在数据库工作内。代码运行在 RequestManager 进程中,继承它的操作系统身份;这个身份能读什么、写什么、连接什么、启动什么,决定了本地影响上限。公告给出了高影响示例,运营方仍需从服务配置、容器策略、文件权限、环境变量、凭证存储和网络控制中测量自己的真实上限。

检测也受同一时间顺序支配。失败的 SQL 只是一种可能的下游痕迹,并非 Python 求值必然留下的信号。表达式可能在解析时失败,可能返回不合适的对象,可能先产生副作用再失败,也可能返回下一层能够接受的内容。只记录数据库错误,会丢掉序列开头;请求身份、被拒字段和进程遥测必须在同一条时间线上会合。

修复目标因此也不该叫“更安全的 eval”。报表功能不需要调用者提供算术、函数调用、推导式或属性遍历;它只需要一个模型列,并在排序时使用程序预先选好的两种方法之一。彻底撤掉这门多余语言,比试图从 Python 表达式语法中切出一小块“安全区域”更容易证明。

默认命名空间决定了这个缺陷实际运行在哪个执行上下文中。RequestDB 是普通 Python 模块,会导入模型、SQLAlchemy 函数、日志组件和应用辅助函数;eval() 能解析环境中可用的名称,Python 还会按文档规则自动提供内置对象。公告已经说明属性遍历可以走到命令执行。这意味着攻击者控制的属性表达式会以服务 Python 进程的权限求值,并能继续接触该命名空间中可达的对象;修复差异证明补丁撤掉了解释过程,而不是试图过滤某几种字符串。

只允许访问 ReqDB 表的数据库账号,可以降低恶意 SQL 语句的后果,却几乎约束不了数据库驱动之外的 Python 动作;锁得很紧的容器也许限制了主机影响,挂载给应用的凭证仍可能十分重要。评估时应分别列出解释器执行、进程身份、容器或主机隔离、数据库角色、凭证挂载和网络策略,才能得到具体影响上限,避免把其中一项控制误当成万能沙箱。

要在实验里证明这段先后顺序,可以直接使用上游回归测试里的无害继承名称。测试在 RequestDB 辅助函数入口、对应 SQL 的编译点和数据库发送事件各放一枚无副作用计数器,再提交这个名称。正确结果是入口被调用一次,字段解析返回普通错误,对应编译与发送计数保持为零,进程也没有新的子任务或文件变化;随后用合法字段重复一次,计数按预期增加。这样得到的是“非法字段在通用 Python 求值以前、也在对应 SQL 编译和发出以前被拒绝”的时间证据,并把修复位置与最终响应清楚分开,避免把一个错误响应误当成求值从未发生的证明。

3.2 映射模型的 Python 表面,比数据库列宽得多

如果只是把 eval() 换成不受限制的 getattr(model, external_name),语法虽然收窄,对象表面依旧太宽。Python 类自带命名空间,还会从基类继承属性;SQLAlchemy 模型又增加描述符、关系、映射状态和应用方法。报表有权选择的是映射列,不是一个类能够解析出的所有属性。

官方回归测试使用 __class__,恰好把这项差异显出来。它是普通的 Python 继承属性,所以宽泛的属性查找能找到它;但它不属于 Request 或 Operation 的列,在计数、过滤或去重查询里没有位置。上游选择这个无害名称,正因为它无需启动进程或连接网络,就能区分“Python 看得见”和“报表允许使用”。

SQLAlchemy 公布了做出更窄判断所需的元数据。文档说明 inspect(MyClass) 会返回类的映射器,而 Mapper.column_attrs 只包含映射列和 SQL 表达式属性;关系和更宽的描述符集合由其他属性提供。应用可以询问 ORM 究竟映射了什么,不必询问 Python 类恰好暴露了什么。

相较复制一份手写清单,这项元数据检查也更能跟上正常的模型维护。受支持的映射列被重命名、新增或删除时,运行版本中的映射器会反映真实模型。公开接口仍可能需要别名和弃用规则,但最终查找不会悄悄滑入方法或继承属性;若业务只准备公开部分列,本地扩展还可以在映射结果之上进一步收紧。

固定辅助函数实际上设置了两道检查,缺一不可。第一张字典只允许 RequestOperation 两个模型名;映射器检查再只允许所选模型的映射列。如果代码只检查列,却让调用者指定任意模块全局对象作为模型,或者固定模型却接受任意 Python 属性,仍然只完成了一半。

现在可以不用比喻描述报表卡片:外部别名先拆成模型名和列名;模型从两个常量中选择;列名经过一张极小的别名表规范化,确认属于 column_attrs,再由 getattr() 取出。最终得到 SQLAlchemy 映射属性,整个过程没有对调用者提供的表达式求值。

column_attrs 有意比 attrsall_orm_descriptors 更窄。关系属性虽然也经过映射、在应用代码中有用途,拿它当标量报表列却可能改变表连接和结果基数;混合属性还可能执行自定义 Python,或生成复杂 SQL 表达式。固定辅助函数选择的正是列与列表达式集合,与既有报表所需对象相符;产品策略还可以继续排除敏感或不适合公开的映射表达式。

检查成员关系时要使用解析后的别名,报错时却应保留公开名称。对 Status 来说,映射器认识 _Status,客户端认识的则是 Status;只用外部拼写查元数据会破坏合法字段,只在错误里返回私有拼写又会泄露实现细节,让门户维护者困惑。辅助函数同时保留两种名称,既符合模型结构,也维持稳定的客户端词汇。

3.3 四只时钟,分别记录 Python 求值、ORM 构造、SQL 编译和数据库发送

“发生在 SQL 以前”这句话准确,却仍不足以直接拿来测试,必须把序列拆成可以观测的事件。第一只时钟记录 Python 对拼接表达式的解析和求值;第二只记录 SQLAlchemy 收到返回对象并把它纳入查询;第三只记录编译,即数据库方言把 ORM 表达式树转换成语句结构和绑定信息;第四只记录驱动发送或执行。合法请求通常依次经过四个阶段。修复版本收到未知列时,应在第一只时钟根本没有启动前停下:应用只做普通字典选择与映射器成员检查,抛出 ValueError,绝不会要求 Python 解释调用者提供的源码。

Python 文档对 eval() 的定义说明,第一阶段不能被当成无害准备工作。它会解析表达式,并使用传入的全局与局部映射求值;调用者省略这两项时,就按语言规则使用调用环境,其中也包括内置对象。表达式在返回值以前已经能够解析名称、遍历属性和执行运算。RequestDB 调用的不是一台只认识 SQLAlchemy 标识符的解析器,而是让正常应用模块中的 Python 运行时从文本中产生对象。固定代码无需先证明某个具体字符串一定危险,因为产品从来没有提供这门语言的必要。

返回对象又构成第二项独立决定。SQLAlchemy 接收绑定到模型类的映射属性和表达式对象,再构造查询树。Python 求值若恰好返回这类对象,查询可以继续形成,最终 SQL 甚至看起来完全普通;若返回不适合的对象,SQLAlchemy 可能在稍后拒绝。两种结果都不会撤销求值阶段已经发生的事情。因此,解释器被触达以后,数据库审计可能显示一条正常报表语句、一条失败语句,也可能没有语句。SQL 追踪只能描述第二至第四只时钟,无法单独重建第一只。

严谨测试还要把编译与发送分开。SQLAlchemy 可以在不连接数据库的情况下编译表达式,引擎钩子也可能在数据库接收或执行以前观测到语句。固定版本的未知字段用例最好同时记录“对应报表编译为零”和“驱动发送为零”;合法用例则应出现预期的查询族和绑定结构。如果非法字段已经触发编译、只是没有发送,拒绝位置仍晚于补丁所描述的解析性质;如果两项计数都为零、旧进程的求值钩子却被触发,没有 SQL 仍不能证明安全,只说明失败发生得更早。

异常出现的时刻可以直接帮助调查分类。求值阶段的语法或名称解析错误,可能只留下 RequestDB 异常,没有数据库事件;ORM 参数错误发生在 Python 返回对象以后、编译期间或以前;驱动或数据库错误则意味着语句已经准备或发出。修复版本的未知列和未知方向走第四条有意设计的路:狭窄解析决定变成普通 S_ERROR。内部记录要保留异常类别、服务方法、实际版本和最后到达阶段,客户端仍可只收到稳定消息。这样调查人员能够判断哪只时钟真正走动,不会把不同失败都塞进一个通用错误计数。

一份进程能力账本能把第一只时钟转成具体的本地影响,同时不虚构站点已经遭到入侵。先确定 RequestManager 实际使用的操作系统或容器身份,再记录可读配置和凭证位置、可写应用与临时目录、可执行程序与服务控制权限、数据库角色、挂载套接字、允许的出站目标和委派凭证接口。每项能力都注明在受影响时段是否存在,以及是否有独立遥测可以观察其使用。官方公告给出了 DIRAC 服务可能持有的敏感材料示例;只有本地账本能够判断某只工作进程究竟符合哪些示例。

“具备能力”和“观察到使用”必须分列。进程能够读取数据库密码,只能说明潜在可达,不能证明调用者已经读过;关联请求以后出现子进程、文件打开或出站连接,可能增强代码执行假设,却仍需身份、时间和主机上下文。反过来,未观察到事件只有在相应传感器当时开启、时钟同步、保留周期覆盖现场时才有意义。这种克制不会把严重代码执行缺陷说小,却能避免把可能后果写成伪造的取证结论。

修复后的生产监控同样适用四只时钟。固定进程上的未知字段应停在解析器,不产生对应报表语句,也不伴随进程异常;合法报表进入已知 SQL 查询族,不生成子进程,也不读取无关秘密。任何违背序列的事件——未知名称以后仍发生编译、报表方法调用求值器、实际版本不在获准集合,或请求后出现进程副作用——都要立即复核。每项观测都有明确时间位置,这套模型才真正有用;它把含糊的“是否发生 SQL 注入”换成可以逐项核对的服务行为记录。

三张手绘报表卡在远处 SQL 制图桌之前先进入明亮的 Python 解释器机械,修复通道则把尺寸吻合的列部件直接送到查询桌。
图 3|脆弱序列先让 Python 解析分组、过滤与排序材料,此时 SQL 尚未形成;修复序列选择类型化 ORM 部件,再由 SQLAlchemy 组合,不再接受外部表达式语言。

4 旧分支反而显出了产品真正需要的小语法

4.1 TypeStatus 与日期上下限都是有意设计的接口别名

旧代码并非毫无结构。它已经知道公开分组名 Type 代表 Operation.Type,大多数其他选择属于 Request;知道公开名称 Status 要映射到私有 ORM 名称 _Status;也知道 ToDateFromDate 分别对应 Request._LastUpdate 的上界和下界比较。这些显式分支恰好勾勒出产品真正需要的语言。

保留这套语言很重要,因为安全修复不能悄悄破坏运营方赖以管理分布式服务的报表。门户仍应能够按 Operation 类型分组、按 Request 状态计数、限制时间窗口、取得去重候选值并排列明细记录。安全实现改变的是“名称怎样变成对象”,不会要求用户书写带表前缀的 Python,也不会丢掉既有别名。

Type 说明一张平铺的 Request 字段清单并不完整。数据位于 Operation 上,在明细或计数查询中还需要连接两张表。程序必须同时决定模型和连接方式;调用者只提供公开词汇,服务则掌握它到 Operation.Type 的映射,以及让这个词产生正确结果的查询结构。

Status 展示的是另一种映射。公开接口使用友好名称,SQLAlchemy 模型却把它绑定到 Request._Status。v9.1.10 的辅助函数保留显式别名字典,先解析出私有名称,再确认它属于映射器元数据,最后取得列对象。这样既延续兼容性,又不会放行任意以下划线开头的属性。

日期键实际上是伪装成字段名的比较运算。ToDate 表示“LastUpdate 早于这个值”,FromDate 表示“LastUpdate 晚于这个值”。它们值得拥有显式分支,因为两者都不是应由通用相等过滤接收的数据库列。把运算写在程序代码里,也能让时间语义在评审和测试用例中保持可见。

一份实用的接口约定至此可以列出所有类别:获准用于报表的普通 Request 列、Operation Type 别名、Status 名称翻译、两个日期上下限运算和两个排序方向。未知字段默认失败并返回错误;新增报表能力要通过经过复核的映射和测试进入,不能靠扩大兜底表达式的语言能力获得。

4.2 列、比较值与方向需要三套不同的解析规则

动态查询要变得稳妥,第一步就是停止把所有输入都笼统地称作“字符串”。列名从模型元数据中选择对象;比较值始终作为数据进入 SQLAlchemy 表达式;排序方向只在程序拥有的两项操作中选择。它们可以出现在同一个序列化请求中,却有不同的词汇、不同的输出类型和不同的拒绝规则。

列名解析的输出应是映射属性,例如通过元数据成员检查后,由 getattr(Request, resolved_name) 返回对象。给列名字符串加引号很脆弱,数据库方言会变化,而且本案中的 Python 求值发生得更早。返回类型化 ORM 对象后,SQL 渲染交给库完成,原名称中的标点也不可能变成另一段语句。

数据值方面,固定路径在输入为列表时把原 Python 对象交给 column.in_(value),标量则进入 column == value,后续参数绑定由 SQLAlchemy 正常处理;日期仍走明确比较。数据值从不转换成 Python 源码,过滤辅助函数也无需理解字符串、列表、数字或时间戳的文本表示。

排序方面,程序先把方向转成小写,确认它属于 {asc, desc},再在已经获准的列对象上调用已知方法。方向不能作为后缀粘到外部属性名后。未来若需要空值位置或其他排序模式,可以增加具名枚举项,同时提交行为测试和明确的 SQLAlchemy 操作。

4.3 一份报表字段工作簿,在撤掉危险语言时保住业务含义

移除求值能够关闭漏洞,成熟的回移补丁还要保留每个合法报表词真正代表的含义。最实用的材料不是一张平铺允许名单,而是一份字段工作簿:每个公开别名占一行,依次注明模型、解析后的 ORM 属性、允许出现的方法、比较结构、所需表连接、输出标签、可见性策略和代表性测试。例如,Status 解析成 Request._Status,可用于明细过滤和计数,不需要 Operation 连接,公开标签仍保持 StatusType 则解析成 Operation.Type,会改变查询结构,必须用不含 Operation、含一个和含多个 Operation 的 Request 分别测试。

模型与连接必须写在一起,因为单凭列名无法决定结果正确性。在 Request 明细中使用合法 Operation 字段,需要沿着关系把 Operation 记录带入查询;一条 Request 若含两个命中的 Operation,分组项和选择列会决定它出现一次还是两次。源码已经为 Type 保留显式连接和分组集合,工作簿应把这项行为记录为公开含义的一部分,而不是偶然实现细节。未来解析器即使返回正确列,只要漏掉或重复连接,虽然不再存在 eval 注入,仍会向管理员提供错误总数。

比较结构又是一项独立约定。标量使用相等,列表使用 in_(),日期别名分别针对 Request._LastUpdate 选择小于或大于。工作簿还应说明空列表代表“没有匹配”、错误还是跳过过滤;空值怎样处理;服务接受什么时区与时间精度;数字和文本是否规范化。这些决定可以延续当前行为,无需借安全补丁发明新语义,但必须写清,避免回移时以“修复求值”为名悄悄改变运营团队用来判断队列和失败状态的报表。

输出语义同样重要。明细响应包含参数名、有序的字符串记录和总数;计数返回映射,去重候选值返回列表。合法字段回归不能只比较 OK,还要检查参数顺序、记录表示、计数类型、分页稳定性和空结果结构。安全解析器若用另一个键返回相同数据,或以不同方式格式化时间戳,门户仍可能失败。正向夹具因此属于安全发布的一部分:它能避免兼容故障制造压力,迫使团队在上线后恢复旧动态捷径。

字段可见性应作为结构解析后的第二次选择。上游辅助函数允许 Request 或 Operation 上的映射列;站点则可以按服务方法和调用者组定义更小集合。工作簿可以把某字段标记为“已映射但不向门户公开”“只对运维组开放”,或因枚举本身会泄露敏感值而禁止去重查询。策略拒绝应返回普通授权或不支持字段结果,不暴露隐藏列是否存在。把它与映射器检查分开,可以清楚区分“这是不是列”和“这个身份能否在这里使用”。

名称还需要有记录的规范化规则。固定排序辅助函数会有意把方向转成小写,列名除明确的 Status 别名外仍区分大小写;随意裁剪空格、折叠 Unicode 或接受标点变体,会让不同输入汇合到同一字段,也会让审计记录含糊。工作簿为每项受支持转换同时保存提交形式和规范形式,未知拼写默认拒绝。门户如果需要友好的本地化,应在请求离开客户端以前,把显示标签翻译成稳定的接口别名;RequestDB 数据层不应猜测自然语言变体。

工作簿要随服务接口一起版本化。增加、重命名或删除映射列,不一定改变公开接口,因为别名和策略可以保持稳定;反过来,公开别名或接口变化即使没有修改 SQLAlchemy 模型,也可能破坏客户端。发布说明应列明接口层变化、弃用周期、涉及方法和迁移测试。新旧版本并存时,客户端只发送所有服务进程共同支持的交集,或让路由把新词汇固定到新工作进程。版本错位造成的未知字段错误是运营证据,不能成为重新开放表达式求值的理由。

本地扩展最能体现工作簿的价值。某个虚拟组织可能继承或替换 RequestDB,增加映射属性,公开自定义门户报表,或修改授权。源码复核必须在最终软件包中找到这些覆盖实现,把每一行与上游比较。上游文件已经干净,不代表仍把模型、属性或操作格式化进 eval() 的本地方法也得到修复;使用不受限制 getattr() 的本地方法也可能超出映射列约定。因此,构建时清单要沿调用路径和能力查找,再对每个可达消费者运行同一项无害继承名称拒绝测试。

资源策略负责补齐这套语法。一项字段可以安全、也已获准,却仍可能在大型 Request 数据库上产生昂贵的分组、排序或枚举。应设置列表长度、分页大小、时间窗口和请求频率上限;识别需要索引的列;对基数过高的去重查询设置上限或禁用;并在受支持数据库引擎上测量合法查询计划。超限时返回明确服务错误和遥测,不要等查询占满工作进程后才超时。这些限制不能缓解 Python 求值,却能保证类型化替代方案可运营,避免团队因性能绕过它。

工作簿最终也成为评审脚本。每一行都能指向公开别名、准确映射对象、表连接、比较方式、授权决定、正向夹具、无害拒绝和预期 SQL 查询族;没有进入工作簿的输入,则必须在对应 SQL 以前得到解析或策略拒绝。它远小于 Python 表达式语言,又远比一串字符串丰富;既保存产品含义,也让各团队依据同一份行为说明工作,还给未来维护者留下一个明确位置,在不重新引入解释器的情况下增加能力。

暖纸手绘交换台把 Operation 类型、Request 状态和两个日期上下限筹码送进尺寸吻合的模型槽,密封值信封与双档排序拨杆则沿独立轨道前进。
图 4|产品别名走明确分支;列名、数据值和排序方向始终是三种不同材料,不再让一条通用字符串同时承载所有含义。

5 补丁把外部名称变成类型化 ORM 对象

5.1 _get_column() 把两个已知模型绑定到各自映射列

v9.1.10 的差异先把 inspect 加入 RequestDB 的 SQLAlchemy 导入列表,再在类定义开头附近加入静态辅助函数。_get_column(table_name, column_name) 只有一个任务:不对输入求值,直接返回受支持的 ORM 列属性。把转换集中在这个小函数中,安全决定既能复用,也容易单独复核。

辅助函数内部的 models 只把两个受支持、由服务掌握的表类别映射到程序拥有的类:RequestOperation。公开处理器路径由服务选择表类别,外部输入负责选择字段;“未知表”主要用于直接调用辅助函数和回移补丁的拒绝测试。查找失败时会抛出带“未知表”信息的 ValueError,任何调用者都不能让 RequestDB 搜索模块中导入的其他对象,也不能发明带点号的访问路径。

第二张字典把公开名称 Status 映射为 _Status,其余列名保留原拼写。辅助函数随后在 inspect(model).column_attrs 中检查解析后的名称;这个映射器集合只含映射列与 SQL 表达式属性。检查失败会抛出另一种 ValueError,消息中包含表类别和原始外部名称。

两项检查都成功以后,辅助函数才调用 getattr(model, resolved_name)。此刻 getattr() 只负责取出对象,不负责决定它是否获准;显式模型字典和映射列成员检查已经证明结果属于所需类别。正是这套先后顺序,让动态属性访问在这里变得合适。

辅助函数的错误消息也是运营接口的一部分。“Unknown table”把不受支持的模型类别,与“Unknown Request attribute”或“Unknown Operation attribute”区分开。门户可以展示普通失败,回归测试可以断言准确拒绝点,监控也能统计新字段选择,而不必让每份畸形报表都制造异常调用栈。

下游分支回补补丁时必须保留完整决定。只复制最后一行 getattr(),却省略模型和映射器检查,会重新打开宽泛的 Python 属性面;把 column_attrs 换成更宽的描述符集合,也会改变保证。辅助函数、导入、调用点和测试应作为一个整体复核,并记录部署实际使用的源码提交与构建产物摘要。

模型字典还是未来扩展时必须经过的审查点。以后若增加基于 File 的报表,把 File 放进字典确实会让它的映射列在技术上可解析,却不会自动授予每个服务方法访问权。改动必须说明需要怎样连接表、公开哪些字段、哪些身份可以查询,以及结果基数如何测试。把字典成员变化视为显式设计事件,模型增长就不会静默扩大 Web 接口。

5.2 过滤与排序辅助函数让数据和操作各走窄道

_apply_web_filter() 是类方法,因为第一步要调用共享的列解析函数。它接收现有查询、已知表类别、外部列名和数据值;数据值为列表时,返回 query.filter(column.in_(value)),其他情况返回相等过滤。两个分支都不再拼接字符串。

_get_order_expression() 对排序做了平行转换:先解析列名,把方向转成小写,再确认结果只能是 ascdesc,其余值抛出 ValueError;随后在已经获准的映射列上调用排序方法,返回 SQLAlchemy 表达式。

先解析列名、再检查方向不会引入求值,两步都是封闭选择;但如果两个输入同时非法,先后顺序会影响调用者最先看到哪条错误。回归测试需要固定本地客户端依赖的服务行为,监控则应把未知列和未知方向都归为“输入被拒”,不能自动替调用者判定恶意。

公开方法继续保留日期与 Type 的显式分支。明细和计数查询仍会在需要时连接 Operation,也继续直接比较日期上下限;通用过滤分支改为调用 _apply_web_filter(),明细排序改为调用 _get_order_expression()。补丁减少了重复构造代码,同时保留合法查询的表连接和聚合结构。

数据值的验证仍应放在过滤辅助函数上方或旁边。映射为整数、时间戳或枚举的列,可能在序列化、应用或数据库阶段拒绝不合类型的值;超大列表即使安全绑定,也会带来资源压力。应按报表约定设置类型、长度和分页上限。CVE 修复负责消除代码求值,并不意味着每份语法安全的报表都廉价、合理或已经获准。

排序测试除了拒绝,还要检查分页稳定性。按非唯一列排序时,记录变化可能使数据跨页移动,原代码也可能依赖补丁之外的第二排序键。回移以前应使用重复值、最小和最大页大小以及两个方向保存当前输出。运营方能够证明熟悉报表的记录集合与排序保证均未改变,才不容易在发布后因兼容问题紧急恢复旧捷径。

5.3 普通 S_ERROR 让三条报表路径以同一种方式关闭

补丁修改了每个受影响的公开方法,不只修公告最短示例指向的计数路径。明细过滤调用新的过滤辅助函数,明细排序调用排序辅助函数;计数查询只解析一次分组列,再把同一对象用于选择和分组;去重候选值则把 _get_column() 的结果送入 distinct()。八处求值由此从三条 Web 查询路径中全部消失。

每个方法都捕获 ValueError 并返回 S_ERROR(str(e))。DIRAC 客户端原本就理解 S_OK/S_ERROR 结果约定:成功结果带有 OKValue,错误结果则以 OK=false 携带 Message。非法字段因此成为普通服务拒绝,无需依靠 Python 调用栈、数据库异常或特殊传输状态。

计数查询现在只解析一次 groupingColumnType 选择 Operation.Type,其他公开选择在 Request 上解析,Status 别名也由辅助函数处理;同一个对象同时进入 session.query()group_by()。重复解释由此消失,选择列和分组列在结构上完全一致。

去重候选值也获得了真正的表检查。ReqManagerHandler 依旧把 Type 路由到 Operation,把其他公开属性路由到 Request;RequestDB 则验证表类别已知,并确认列经过映射。它不再假设处理器的一次分支已经对 Python 属性作出完整决定。

源码搜索可以帮助验收版本,却有明确限度。v9.1.9 的三项 Web 报表方法中共有八处 eval();v9.1.10 固定版本的 RequestDB 则用辅助函数取代这些构造。仓库其他部分仍可能为另行设计的功能保留求值;扫描结果必须与不可信输入的可达性和预期语法一起复核,才能判断是否与本 CVE 等同。

补丁规模不大,设计含义却很清楚:外部报表名称不再是一段小程序,只是从服务掌握的有限词汇中选择对象。接下来的证明要同时覆盖两面——合法报表仍然可用,而 Python 可以继承、却不属于 ORM 模型的名称,会被明细、计数与去重候选值三条路径一致拒绝。

输入类别服务掌握的语法类型化输出修复版本拒绝方式
模型RequestOperation已知 ORM 类未知表的 ValueError 转成 S_ERROR
解析后的别名必须存在于 inspect(model).column_attrsSQLAlchemy 映射属性未知模型属性
标量值应用数据类型与字段策略绑定参数的相等表达式正常验证或数据库类型错误,绝不成为 Python 源码
列表值所选字段允许的列表column.in_(values)正常类型拒绝,绝不格式化成求值字符串
排序方向ascdesc已知 SQLAlchemy 排序表达式未知排序方向

一致错误返回还需要兼顾运维可见性。页面应得到稳定、可本地化的“未知属性”或“未知方向”结果,服务日志则保留请求关联号、方法名、规范化后的字段类别和工作进程身份,避免记录整段未经信任的原始材料。这样支持人员能区分用户拼写、旧门户与主动探测,又不会把日志变成第二个注入载体。监控可以统计每个入口的拒绝率和来源身份,但告警阈值应建立在升级前基线之上;单次错误不是利用证据,持续异常也只能触发调查,最终结论仍要与认证、进程和主机记录相互印证。

两座手绘模型档案柜把卡片送入映射列索引,一把继承属性的影子钥匙被挡在门外,旁边排序拨杆只有向上和向下两档。
图 5|固定闸门先选 Request 或 Operation,再验证映射列,随后应用数据比较或两种排序操作之一;更宽的 Python 属性面留在门外。

5.4 三条维护线要守住同一项性质,不必拥有相同行号

公告列出的三个修复版本,是三条不同维护线上的发布对象。复核时,它们准确的内容身份分别为 v8.0.79 的 a05a64be9fbf6119080b735ae0a4b2d13cf67a48、v9.0.22 的 8f2dcddb4310790da01299da9cad895423b91669,以及 v9.1.10 的 2f5c5f3b74ab65b3b7d5687d561bc8deee1e4859。即使其中某项提交说明只提到发布记录或依赖固定,这些完整提交仍是很有价值的不可变视图:附着在提交上的源码树才是复核对象。可移动标签、软件包文件名或分支头只是定位手段,完整提交号再加实际构建产物摘要,才构成内容证据。

9.0.22 与 9.1.10 源码树都把三个解析辅助函数放在 RequestDB.py 第 192–221 行相同范围;8.0.79 的等价代码则位于第 194–223 行。两行偏移没有意义,重要的是决定相同:从程序自己的字典选择 Request 或 Operation,转换 Status,要求名称属于映射器 column_attrs,把数据值原样交给 SQLAlchemy,并把方向限制为 ascdesc

回移补丁要按这项性质验收,不能按补丁外形验收。旧分支的导入、类结构、SQLAlchemy 行为、结果辅助函数或格式可能不同,逐字套用既可能失败,也可能产生错误。只要每项外部模型和列选择最终都落到有限映射集合;数据始终是数据;排序操作保持有限;明细、计数和去重中的八个语义位置全部消费类型化对象,辅助函数可以改名,结构也可以不同。反过来,一份能够干净应用、却仍留下动态分组表达式的补丁仍不完整。

回移先从目标分支自己的导入与映射开始:确认 inspect(model) 返回映射器,column_attrs 包含该分支报表真正使用的属性,公开 Status 指向正确的私有映射,Request 与 Operation 也绑定到正确模型。较旧 SQLAlchemy 版本若检查方式不同,可以调整实现,却不能把范围扩大到关系、全部描述符或任意类属性;不受限制的 getattr() 不是兼容层,而是放弃补丁本应建立的性质。

映射检查成立以后,复核沿每个消费者继续:明细中的通用列表过滤、标量过滤与排序,计数中的分组列、列表与标量过滤和分组表达式,以及去重候选值中的所选列都要消费类型化对象。这里统计的是语义角色,不只是 eval 出现几次,因为下游分支可能把解释过程移入辅助函数,或换成另一种执行原语;最终 wheel 或镜像里的本地 ReqManagerHandler 与 RequestDB 覆盖实现也在同一条路径上。验收结论应写明准确位置和角色,不能用一次仓库关键词总数冒充可达性已经消失。

这条路径最后回到客户端看到的成功与失败约定。未知 Request 字段、未知 Operation 字段、不受支持的表和非法方向应成为稳定服务拒绝,不返回调用栈,也不产生对应数据库语句;合法别名则继续保持响应结构、表连接、计数和顺序。目标分支若沿用较旧 S_ERROR 约定,就以门户实际收到的序列化结果验收。未捕获异常即使阻止了代码执行,也会制造运营中断并诱发匆忙回滚,因此兼容性本身也是让修复留在线上的条件。

底层修复历史让分支证明还能更精确。9.1 分支先由提交 628700c68564a723a147feb2e374fe5aa97832c4 加入正向报表矩阵与三项继承名称断言,此时 RequestDB 仍是脆弱实现;它的直接子提交 8ec11072f48103d94e8b7cca831e2c7ce304d6c7 才从 RequestDB 移除求值;后续 88501cdcb851bad352193e5e3b2e3e0f0ffa79d7 只调整相关文件的格式与 Pylint 标记位置。这个顺序很有价值:安全断言先针对当时会失败的行为写下,随后修复才让它通过。

另外两条并行修复提交分别为9.0 分支的 a6f75ce5d369254d46ca6f1d39e3f384366fa396,以及8.0 分支的 99f1af88b525e69a77f204aebf7d8df3b6056142。在本次复核的发布对象上,9.0.22 与 9.1.10 含有完全相同的修复 RequestDB blob,它们的处理器 blob 也与 v9.1.9 完全相同。这组身份把架构变化隔离得很清楚:面向传输的方法外形保持稳定,RequestDB 变成语义检查点。blob 相同是很强的源码证据,却仍不能说明某次发布任务实际跑过哪些测试,也不能证明线上进程加载了哪个文件。

测试快照还带来一项重要限定。公开的 test_web_queries_reject_unknown_attributes() 用例存在于 v9.1.10;v9.0.22 的生产 RequestDB blob 虽已修复,发布快照中的 RequestDB 测试却仍是旧内容;v8.0.79 测试快照同样没有新用例。因此,准确说法应是“v9.1.10 携带公开回归用例,v9.0.22 携带完全相同的修复实现,v8.0.79 携带等价的分支原生实现”。运营方仍需亲自在 9.0 与 8.0 构建产物上运行拒绝矩阵,不能把 9.1 存在测试借给其他分支,冒充执行证据。

8.0 尤其需要使用分支原生正向数据。它的 Request 映射含有 DIRACSetupOwnerDN 等字段,本次复核的 v9 明细则含有 Owner。机械复制 v9 夹具会因合法 v8 模型差异失败,还可能诱使维护者放宽解析器。正确办法是从目标分支自己的映射列构造合法用例,保留该分支公开别名与表连接,再复用“继承属性停在 column_attrs 之外”这项安全断言。可以迁移的是不变量,业务词汇与行号无需相同。

6 一座安全实验室证明请求究竟停在哪里

6.1 官方测试用真实映射记录对照继承属性 __class__

v9.1.10 的回归用例使用内存中的 SQLite 数据库,并通过 RequestDB 的真实 SQLAlchemy 映射接入。它创建一条用于 Web 明细报表的 Request,加入目标为合成存储端点的 RemoveReplica Operation,再附上一份带测试路径的 File,最后通过 putRequest() 写入整组对象。File 让存储层级更具代表性,却不会被受影响辅助函数查询;这份夹具的安全价值来自真实参与测试的 Request 与 Operation 映射、表连接和错误传播。

合法明细用例按 Type 过滤,把 RequestID 设为升序,并期望得到一条记录;合法计数用例按 Type 分组,以 Waiting Status 过滤,并期望 RemoveReplica 计数为一;合法去重用例则向 Operation 查询 Type,期望取得同一值。这些断言保护的是安全改动后用户仍然需要的能力。

拒绝用例把 __class__ 放在三个位置:明细查询把它当作过滤键,计数查询把它当作分组属性,去重查询把它当作 Request 列。三项结果都必须把 OK 设为 false,并携带完全一致的消息“Unknown Request attribute '__class__'”。这个字符串对 Python 类可见,却不在映射列元数据中。

这项安全测试选得很好,因为它不用可执行载荷就能验证关键拒绝。若修复只是盲目切换到宽泛的 getattr(),用例会立刻暴露问题;同时它不会生成进程、读取文件或发起连接。下游测试还可以加入其他无害的继承名称或未知名称、空字符串、大小写变体、未知表类别和不受支持的排序方向。

官方测试证明了预期源码行为,却不能证明每个线上工作进程都已加载该源码。长时间运行的服务可能在磁盘文件升级以后,仍把旧模块留在内存中。实验室因此还需要第二个维度:让两只隔离进程分别加载旧版和候选版本,用软件包内容、进程启动时间和实际响应显示“已经安装”和“正在运行”之间的差异。

6.2 两只隔离进程显出软件包与运行代码之间的缝

回归环境只使用合成记录,不放生产配置或委派凭证,并通过拒绝出站的策略关闭网络访问;两只一次性的 RequestManager 工作进程放在测试专用路由器后。一只代表修复前产物,用于源码行为比较;另一只加载计划发布的修复版本。进程号、镜像摘要和启动时间写入测试账本,不混进请求数据。

旧进程完全不需要接收带命令的表达式。合法字段用于建立报表基线,无害继承名称与观测计数器则说明旧路径会先抵达求值函数,再在后续失败。实验环境在操作系统接口处拦截进程创建、文件写入和网络访问;测试要问的是控制流抵达哪里,不是危险动作能不能成功。

修复进程接收完全相同的合法用例矩阵和无害拒绝名称。受支持的 Request 列、Operation Type 别名、Status、两种日期上下限和两种排序方向必须保持响应结构;不受支持的模型、列与方向,则必须在通用 Python 求值、对应 SQL 编译与发送,以及副作用观测点触发以前返回 S_ERROR

接着只更新旧进程能够看到的软件包文件,不重启进程。即使在命令行中已经看到修复版本,它的启动时间和内存中模块的行为仍应保持旧状态。把实例移出流量并重启以后,拒绝响应才应变成修复辅助函数的消息,加载源码的摘要也应与发布产物一致。这一步专门抓住“磁盘已更新,所以服务一定已修复”的常见错觉。

测试还要通过预发布环境实际使用的协议和路由层重跑。直接单元调用证明 RequestDB 逻辑,DISET 或门户路径则证明序列化、处理器转发、身份上下文和负载均衡没有改变结果。网关附上关联编号,并把它一路带进服务日志、SQL 观测和进程遥测。

旧进程上的观测装置必须安全失败。只能在一次性环境内打补丁或挂钩,让进程、文件系统和网络原语全部返回有记录的拒绝,并使用无特权临时账号运行;无害继承名称已经足够证明控制流抵达求值器。如果测试装置意外请求真实资源,应立即停止用例、保留追踪记录,修正隔离后再继续,绝不能把环境泄漏误当成测试成功。

同一实验还能验证构建产物来源。从部署实际使用的镜像仓库拉取候选版本,核对签名或摘要,不挂载开发者检出目录直接启动;再通过受控诊断读取运行中解释器加载的模块文件与版本,与构建清单比较。这样能够发现可变标签、分层镜像残留旧软件包,或启动脚本选中了另一套虚拟环境。

6.3 请求、SQL 与进程遥测必须讲出同一段同步故事

请求层应保留时间戳、已认证身份、来源地址、策略允许时的会话或令牌标识、服务方法、分组别名、过滤键集合、排序字段与方向、结果状态、耗时、关联编号和服务版本。调查字段选择时,无需把可能含秘密的比较值全文复制到安全数据湖;键名和数据类型通常已经足够。

应用层要区分“解析时拒绝”和“下游执行失败”。修复版本应以同一关联编号记录结构化的未知表、未知属性或未知方向;旧版追踪中可能出现 Python 或 SQLAlchemy 异常。公告提到利用尝试可能在 RequestManager 日志留下异常输出,但没有调用栈并不能排除代码执行,因此异常搜索只是证据流的一部分。

数据库层先在隔离测试中保存语句指纹和绑定参数数量,再建立兼顾隐私的生产基线。修复版本拒绝列名时,不应出现对应报表查询;合法请求应产生带预期表连接的稳定查询族。原始数据值与完整 SQL 文本可能泄露用户或文件信息,多数情况下,指纹和选定元数据已经够用。

主机或容器层则把 RequestManager 事件与子进程创建、可执行文件映射、命令解释器调用、异常文件读写、配置和凭证路径访问、出站连接、服务重启与日志改动对齐。具体来源可以是 auditd、eBPF、systemd、容器运行事件或终端检测组件;覆盖范围和保留日期必须写入调查记录。

字段中的特殊标点、双下划线名称、类似调用的字符和密集拒绝,可以提高复核优先级,却不是利用已发生的自动证明。扫描器、配置错误的门户或未来合法字段都可能产生奇怪名称;精心选择的表达式也未必符合简单特征。检测需要把身份、方法、字段新颖度、响应、进程行为和部署版本连接起来。

发布以后,“未知属性”事件会成为持久控制信号。重复尝试、新调用者、高价值服务身份,以及拒绝后紧跟进程或网络活动的序列,应触发告警;一次性错误可以进入较低严重度的工程视图,帮助开发者寻找旧客户端。同一套遥测由此同时支撑事件响应和回归监控,不会把每份畸形报表都判成攻击。

时钟质量决定这些记录能否可靠关联。网关、服务、数据库主机和传感器都要记录足够精度的 UTC 时间,持续监控 NTP 状态,并同时保留事件发生时间和采集时间;时钟有差异时,注明测得的偏移量,使用扩大后的时间区间搜索。一台主机发生漂移,就可能让“请求后出现进程活动”的序列前后倒置,过窄的时间连接也会漏掉真正配对。

日志必须避开被调查账号的控制。公告指出,服务身份如果掌握本地日志文件,证据可能被清除;安全相关的 RequestManager 事件应转发到只追加存储,或由不同管理员控制的系统,并监控日志缺口和异常截断,采集健康状态则保存在服务主机之外。本地缺一行时,它会成为有记录的证据缺口,唯一事件副本不会随之消失。

用例预期服务结果预期 SQL预期进程影响
Request 映射列S_OK,保持原响应结构一类获准查询仅有正常服务工作
Operation TypeS_OK,包含必要连接获准的 Request–Operation 连接
Status 别名S_OK映射到 _Status 的条件
FromDate/ToDateS_OK显式 LastUpdate 上下限
继承 __class__S_ERROR,未知 Request 属性对应查询为零
未知表S_ERROR,未知表
不受支持的方向S_ERROR,未知排序方向排序查询为零
未重启的旧进程仍表现为旧行为,说明发布尚未完成可能不符合修复版本预期移出流量、保全证据、重启

6.4 三项独立判据,让熟悉的错误页面不再冒充安全证明

可靠的回归结论来自三项不能互相替代的观测。响应判据记录服务是否返回预期 S_OKS_ERROR 结构;查询判据记录对应报表语句是否编译、发送和执行;进程能力判据记录服务有没有尝试无关的文件、进程、凭证或网络动作。看起来属于修复版本的错误页面只回答第一项问题。旧代码可能在求值以后仍返回错误,测试装置也可能意外压住数据库动作,却漏掉另一种进程影响。三项判据必须绑定同一个关联编号、同一件构建产物和同一只工作进程。

夹具准备完成以后才开始观测。上游 v9.1.10 用例先创建表,再写入合成 Request、Operation 与 File 层级,随后才调用报表;这些准备动作本来就会产生合法 SQL。先预热连接、完成夹具插入并确认预期记录存在,再清零求值、编译、驱动发送和进程能力计数器,最后给单项报表用例分配新的关联编号。否则,夹具流量会让正确拒绝看起来仿佛查询过数据库,另一项用例遗留的计数也可能让合法报表显得危险。清零事件本身要保存,让下一位复核者准确知道测量从哪里开始。

三个消费者的固定控制流并不相同,因此需要分别定义查询判据。明细方法的第 700–748 行先创建基础 SQLAlchemy 查询,通用过滤到第 740 行才进入解析器,执行 .all() 位于第 748 行;所以非法过滤可以与一个已经创建的查询对象同时存在,却仍不发生驱动发送。计数方法在第 800–804 行解析非法分组字段,早于第 806–808 行构造计数查询。去重方法在第 852 行_get_column() 作为参数求值,解析失败会阻止查询调用完成。因此,“没有 ORM 对象”不是三条路径共同成立的说法;“被拒报表没有对应驱动发送”才是。

相信一个零值以前,先重放 6.1 节的三项合法用例,确认服务响应、预期表连接或查询族、绑定参数、记录数量和无关进程能力事件均符合基线。编译监听若在合法查询上什么都看不到,它在拒绝用例中的零就没有价值;工作进程或关联信息若在抵达数据库事件以前丢失,也无法把语句归给当前用例。

随后重放同一节的三项 __class__ 对照,让明细、计数和去重候选值都返回准确的未知 Request 属性结果。数据库观测按关联编号与报表查询族收窄,连接健康检查、事务记账和其他后台语句不属于这份被拒报表。非法排序方向与辅助函数级未知表另作两项性质测试,后者也不能被误写成公开接口直接接受的表名参数。

在结果记录中明确解释“没有 SQL”,不要把它当作速记。最强、也最便于跨环境复现的观测,是被拒报表的对应游标发送或执行次数为零。只有编译监听已经可靠安装、通过合法查询正向证明并正确限定范围时,“编译为零”才是额外实验结论。查询对象创建、表达式构造、编译、游标发送和数据库执行是不同事件。固定源码控制流能够证明拒绝早于对应执行;更早阶段的实测零值属于当地实验,不能自动推广到每个部署和 SQLAlchemy 版本。

进程能力判据无需执行表达式,也能保持安全。在一次性旧工作进程中,观测实际加载的 RequestDB 模块所引用的准确求值器,让入口先增加计数,再立即拒绝继续;操作系统接口同时拒绝并统计进程创建、文件修改和出站连接。负向输入只使用上游继承名称,用来判断旧路径是否抵达求值器;修复进程完全不应触达该计数器。正式依赖结果以前,先用隔离的无害单元夹具验证钩子,因为若替换了另一个导入符号,模块仍可能保留原本地引用,制造虚假零值。

拒绝测试还要配合更宽的正确性矩阵。一条 Request 放入多个 Operation,其中包含重复类型,另准备一条没有 Operation 的 Request;测试空结果、标量与列表过滤、数据值中的引号和 Unicode、两种日期上下限、两种排序方向、重复排序值、最小和最大分页,以及分页稳定性。解析器可以安全,回移补丁仍可能改变连接基数、别名、绑定或顺序。保存这些输出,既能保护运营方依赖的报表行为,也能避免一份测试不足的修复上线后因兼容故障产生压力,促使团队重新引入动态构造。

矩阵必须针对部署实际使用的构建产物运行,不针对开发者检出目录。保存软件包或镜像摘要、完整源码版本、已加载模块路径和摘要、Python 与 SQLAlchemy 版本、数据库适配器、进程启动时间和路由身份。对 v9.1.10 来说,公开用例第 47–78 行提供了正向与继承名称基线。9.0.22 源码树含有完全相同的固定 RequestDB 内容,但该发布快照没有携带这项新测试;8.0.79 则具有等价的分支实现和不同合法模型字段。因此,两条分支必须运行各自原生用例,不能借用“9.1 存在测试”替它们作证。

每项用例最终形成一行可复现结果:工作进程身份、构建摘要、用例名、服务响应、求值器入口次数、能够可靠测量时的对应编译次数、游标发送次数、进程能力事件数量、合法查询族和结果基数;原始事件与观测器配置一并附上,再让另一位工程师至少重放一项成功和一项拒绝。实验室能够证明一件构建产物在一只进程上的行为,却不能替生产环境数清共有多少份副本。因此,最后一行要直接进入实例群账本,由路由状态和逐进程身份决定这份证明还要重复多少次。

两条平行手绘线路中,上层三角卡已经越过两扇打开的木闸进入压模机并通往查询桌;下层不规则深色卡在锁闭红闸处被拒绝,后方线路断开,三只仪表停在起点。
图 6|尺寸吻合的合法卡通过开放闸门;继承属性卡在映射器的 column_attrs 闸门检查处被拒绝,通往压模机和查询桌的线路留空,观测表归零。隔离实验再用关联日志、SQL 编译与发送计数器验证这幅图表达的结果。

7 单元测试变绿以后,全量排查才真正开始

7.1 把版本、身份、端点与进程权限放进同一份清单

从一份可复现的清单开始,不要拿软件包命令截图交差。列出每个中央 RequestManager、开发实例、实验专用部署、灾备环境、容器副本和可能重新上线的休眠镜像;每项记录主机名或工作负载身份、入口地址、区域、负责人、环境、软件包来源、版本、源码提交、镜像摘要、Python 环境和进程启动时间。

每项安装都要与公告的三段约束比较:>= 6, < 8.0.79>= 8.1.0a1, < 9.0.22>= 9.1.0, < 9.1.10,对应首个修复点为 8.0.79、9.0.22 与 9.1.10。前文代码与处置地图已经把这些发布对象固定到完整提交;实例清单这里记录的是本地实际采用的源码与构建产物身份。厂商回移补丁可以接受,但必须以有记录的源码差异和测试证明它具备相同的 RequestDB 安全性质。

版本与可达性应写在同一行。记录 DISET 和 Web 路由、负载均衡池、内部代理、防火墙路径与管理隧道。没有出现在公共 DNS 清单中的入口,仍可能被大范围科研网络或自动化平台访问;配置中已经关闭、却留在旧镜像里的服务,也可能在回滚或恢复时重新出现。

随后枚举受影响时段内能够调用报表方法的身份,包括个人证书或账号、DIRAC 组、联邦身份映射、机器人身份、服务令牌、门户后端、定时报表任务,以及权限可能残留的离职协作者。授权配置和身份提供方历史都要保存,因为今天的组成员关系无法重建半年前谁拥有调用权。

对每个服务进程,记录操作系统账号、容器用户、可读配置、环境变量、挂载凭证、数据库角色、委派凭证访问、可写路径、本地日志位置、服务控制权限和出站网络范围。这些正是表达式一旦被求值会继承的权限。清单不复制凭证原值,只记录标识、位置、负责人和轮换办法。

最后按实例定义受影响时段。起点是脆弱构建产物第一次投入服务,工单建立日期与此无关;终点是最后一个处理请求的进程加载修复代码,旧副本也无法再接收流量。软件包安装、镜像发布、Pod 创建、进程启动、进入负载均衡池和回滚历史分别属于不同时间点,证据表必须全部保留。

发现实例要依靠几份相互独立的清单。配置服务可能知道命名的 RequestManager,编排平台知道运行中的工作负载,镜像仓库知道可部署镜像,证书或 DNS 记录会暴露旧入口,网络遥测还能找到当前清单中没有、却仍被调用的地址。使用稳定的工作负载身份和负责人对齐这些来源;版本尚未查明的入口也必须成为待调查事项。

客户端与服务端证据要分开。门户软件包也许已经完全更新,配置中的 RequestManager 地址却仍指向旧中央服务;修复后的中央服务也可能经由陈旧代理或 DNS 别名被访问。记录各主要客户端群体真正选择的入口,再沿路由追到实际处理请求的进程,避免把正确的客户端版本误当成服务端求值已经消失的保证。

7.2 软件状态、暴露状态与事件状态,回答的是三个不同问题

实例群账本只有在推动决定时才真正有用,不能停在一份资产表。每一行都沿三个互相独立的方向分类:软件状态是受影响、上游已修复、语义等价回移或未知;暴露状态是可达、有意撤下或尚未证明;事件状态则是发现证据、在明确范围内未见迹象或仍无法判定。一项令人放心的事实不能同时关闭三列。安装修复代码只改变软件状态,收紧授权改变暴露状态,历史检索只改变事件判断。

落在三段公开受影响区间内的原版构建产物,只要正在或可能经报表路线提供服务,就需要永久升级或经过验证的回移。初始记录至少包含准确版本与摘要、路由和调用者人群、负责人及修复产物计划。面对一项认证即可触发、能够经网络到达的严重缺陷,“等看到可疑请求再修”并不是合理判据。发布可以按当地可用性约束安排,但在修复代码真正加载以前,软件这一行始终保持打开。

受影响产物若已经停用或被认为不可达,改变的是暴露状态,不是代码状态。保持所有路线撤下,验证方法授权和网络控制,把产物从回滚与恢复流程中封锁,并在重新启用以前完成修复。“没有公共 DNS”“页面按钮已隐藏”和“没有看到流量”都不等于正面证明路线关闭。可信的不可达结论要包含配置快照、路由成员关系、授权策略,以及指定身份经每条已知入口发起后得到拒绝的受控测试。

采用官方修复版本或已知携带同项修复的后续构建,只在每只工作进程都加载它以后才能满足软件决定。逐进程记录构建摘要、模块版本、启动时间、进入路由的区间、一份合法报表和无害拒绝结果。修复文件已经落盘、旧进程仍存活时,现场属于新旧混合,不是结案。负载均衡器若不能识别由哪只进程回答,就把实例行保持为未决;聚合地址的一次修复响应不能替相邻进程作证。

厂商或本地回移在证明语义等价以前都只是暂定修复。材料必须包括八个查询构造角色全部移除,Request/Operation 模型选择、映射器 column_attrs 成员检查、标量与列表数据直接处理、有限排序、普通服务错误、分支原生正向行为,以及最终产物摘要。版本后缀写着“patched”,或工单引用了 CVE,都没有提供这些证据。即使基础上游版本已经修复,构建包内的本地 RequestDB 或处理器覆盖实现也必须另行检查。

处于公告所述版本世系之外,也要使用谨慎语言。高于修复点的维护版本,在版本祖先或源码能够证明继承修复时可以接受;远早于受影响区间的停止维护版本,却不能自动宣告安全,它可能拥有不同代码,也没有安全支持,现有测试甚至无法与之有意义比较。应向维护者取得判断,或迁往受支持修复分支。“不在公开约束内”只是漏洞数据库分类,不是普遍安全保证。

观察到的状态修复决定结案以前需要的证据
受影响原版正在或可能提供报表立即升级或验证回移摘要、路由、调用者集合、负责人和修复产物计划
受影响产物已经撤下保持撤下,修复后再启用全部路线拒绝、策略版本、回滚与恢复副本封锁
每只进程都加载官方修复代码软件修复完成,历史复核另行推进逐进程版本、启动时间、合法报表与无害拒绝
磁盘已修复,但进程陈旧或混合尚未关闭路由成员关系、逐进程重启和行为证据
声称存在厂商或本地回移暂定八个角色、类型化解析、分支测试和产物摘要
修复基础版本含有本地覆盖版本不足以证明覆盖实现的调用路径与最终包复核
产物、入口或进程身份未知处置项保持打开负责人、期限、临时状态和分类所需证据

可达性应建模成一组路线,不能只留一个“是/否”格。纳入浏览器到门户再到 DISET、直接 ReqClient、定时报表任务、内部管理地址、旧 DNS 或证书别名、管理隧道和恢复环境;每条路线注明来源人群、认证交接、方法集合、目标池和实际进程证据。ReqProxy 应进入拓扑与恢复清单,因为它可能暴露陈旧路径、凭证或镜像;本次复核的求值点仍位于中央 RequestManager 的 RequestDB。这样既把网络账目做全,也不会把脆弱函数错安到相邻组件。

身份也需要两份相连账本。一份记录入口处的人或自动化主体、证书或令牌标识、DIRAC 组和虚拟组织映射、有效期与门户会话;另一份记录 DISET 真正授权的身份,它可能是一项共享门户后端。两者之间的关联必须保存。今天的组成员无法重建过去谁有调用权,因此证书有效期、令牌签发、组变更、离职协作者和门户审计事件都要带生效日期保留。

事件状态只能从按进程定义的受影响时段开始:起点是受影响产物第一次进入路由,终点是通往最后一只进程的最后路线关闭,或它加载了修复代码。范围与保留周期都清楚的日志检索,可以支撑“在该人群和时段内未见迹象”;它不能修复产物,也不能证明另一条未记录路线不可达。关联到进程副作用的证据会改变事件响应,不会改变源码版本。三列分开,才能用准确语言描述现场,也不会让今天已经修复的部署抹掉理解过去的需要。

“未知”是一项需要行动的状态,不能成为选择最方便答案的借口。每件未知产物、入口、身份映射或工作进程都要有负责人、期限、临时处置状态,以及用于分类的准确证据;价值高或可达范围大的未知项优先处理。材料到位时,只更新它真正回答的那一列。这样,下一节的任务就很清楚:临时措施能够在构建修复产物期间降低暴露,却不会改写这里记录的软件状态。

7.3 每条替代路线都经过测试,临时措施才配得上这个名字

临时处置有严格优先顺序。运营允许时,先撤下受影响的报表方法;必须保留时,把调用者缩小到确有需要的具名身份;随后再降低 RequestManager 进程权限,并在构建修复产物期间改善证据采集。每项动作都只能换来一段可问责的维护时间,不会改变受影响源码。例外记录应明确写成“固定代码上线前的临时控制”,不能只写一个没有期限、负责人和测试的“已缓解”。

部署能够可靠表达方法级授权时,这是最清楚的控制。明细、计数和去重候选值要分别设策略,因为用户即使从未提交最终报表,门户也可能在页面加载时调用最后一项。对每个方法分别使用一个获准身份和一个被拒身份测试,保存实际生效的配置版本,并记录中央服务返回结果。只在浏览器界面检查的规则管不到直接 ReqClient 或另一套门户后端;若策略无法稳定区分这些方法,撤掉报表路由会更可信。

每条替代入口都必须显式测试,包括可见门户、直接 DISET 或 ReqClient、共享后端调用、内部负载均衡地址、旧 DNS 与证书别名、定时报表身份、管理隧道、重试路线和灾备环境。先用指定账号和无害合法请求确认允许路线,再用指定拒绝账号确认请求早于 RequestDB 停止,并记录由哪只工作进程、哪份策略回答。一条路线没有出现在近期流量里,不代表它已经关闭。

共享门户后端尤其需要谨慎。如果 DISET 授权的是该服务凭证,把 RequestManager 只开放给后端,不过是把决定向前移了一跳。门户必须保留真实用户身份,执行自己的用户和字段策略,防止后端凭证被直接复用,并生成把浏览器主体与服务调用连接起来的关联记录;否则,所有门户用户仍可能借同一张获准证书抵达受影响方法。这样的结构能够降低网络暴露,却可能仍完全满足公告中的“已认证”前提。

能够理解应用请求的网关,只有在读懂解码后的规范结构时才适合增加第二道过滤。按照门户约定验证准确公开字段别名、数据外形、排序方向、列表长度和分页大小,未知键在转发以前拒绝,同时记录规范化决定;真实栈接受的不同序列化与编码形式都要测试。只在原始字节中搜索下划线、标点或 Python 单词的设备,没有验证产品语法,还可能在 JSON、DISET 和日志表示之间表现不同。

标点黑名单与限速都只是次级控制。黑名单试图识别产品从不需要的通用语言片段,另一种表示或未预料表达式就可能绕开;限速能够减少嘈杂探测和昂贵报表突发,却挡不住一份已认证请求抵达求值器。两者若有检测与可用性价值可以保留,但不能据此把软件状态从受影响改成已修复。它们的成功标准是可测量的拒绝与流量下降,不是危险调用点已经消失。

影响控制也必须具体。使用 RequestManager 的真实运行身份验证不必要出站目标已经拒绝;卸载不用的凭证挂载和可写目录,移除软件包管理、服务控制与主机套接字权限,收紧数据库角色,并把安全日志转发到服务账号无权修改的位置。每一项保留能力都注明支撑的业务功能和能够观察使用的传感器。一句笼统的“最小权限”无法代替前后可比较的清单。

处置不能毁掉历史记录。重启可疑进程、轮换凭证或删除临时文件以前,先保存路由状态、需要时的进程和网络信息、服务与身份日志、策略版本及受影响构建产物;采集对象要计算哈希,时钟偏移和保留周期也写入记录。维护期间继续向外转发新的拒绝与路由事件。如果唯一一份本地日志归同一服务身份控制,就先复制到独立管理存储,再让它支撑“未见迹象”的结论。

每项例外都要有负责人、开始时间、失效时间、覆盖的方法和路线、获准身份、策略摘要、测试结果、监控查询与退出条件。配置、身份映射、门户或负载均衡变化以后重新测试,因为它们无需修改 RequestDB 就可能重开路线。即将到期的控制要提前通知负责人;悄悄续期会让紧急措施变成没有记录的常设结构。业务若要求扩大调用者集合,应留下决定并加速发布,不能无声削弱规则。

退出必须依靠代码和进程证据。每只承接流量的工作进程与备用实例都加载上游修复版本或验证过的回移;合法报表保持结果;继承字段和非法方向停在类型化解析器;回滚与恢复产物携带同一修复;临时授权或网关例外也已经移除,或转成普通、理由充分的长期策略。撤除前后比较路由和拒绝遥测。这些控制只是为下一步滚动替换争取时间,真正结束它们的,是替换解释器路径。

7.4 只构建一次,摘除旧进程,重启并证明实际服务代码

  1. 发布验收从一份经过评审的源码对象和一件不可变构建产物开始。解析预期的依赖锁或软件包元数据,运行上游与本地回归测试,生成组件清单,并附上源码提交、构建任务身份和摘要。检查最终 wheel 或容器,确认 RequestDB 包含三个新辅助函数,受影响的 Web 方法也不再用 eval() 构造表达式。
  2. 同一件构建产物应先进入具有代表性 DISET 路由、认证方式、ReqDB 模型和 SQLAlchemy 版本的预发布环境。完整的合法与拒绝用例矩阵都要运行,覆盖分页、表连接、日期上下限和两种排序方向。辅助函数单测变绿,不能代替一条真正经过序列化、处理器转发和计划发布数据库适配器的门户请求。
  3. 发布必须逐实例记账。先从路由中移除一个旧工作进程,按服务策略等待或终止在途报表请求,再用不可变产物替换或重启;记录新进程号、启动时间、镜像摘要和健康证据后,才把它放回负载均衡池。重复这一过程,直到池中不含任何旧进程。在共享存储上执行一次软件包升级,不能满足这项要求。
  4. 新旧版本并存期间,应把无害的继承名称探针有目的地路由到各实例,并记录由哪个版本回答。不要向生产用户广泛发送未知字段,也不能看到一次负载均衡后的正确错误,就认定所有副本都已修复。健康元数据或经过认证的诊断接口应暴露服务版本和进程启动时间,让探针能够对应单个工作进程,同时不泄露敏感配置。
  5. 回滚产物也要经过相同审查。过去“运行良好”的镜像如果仍带脆弱 RequestDB,自动回滚会在成功升级几分钟后重新打开解释器路径。应准备包含修复的回滚候选版本并运行测试,让部署策略拒绝缺少来源证明或 RequestDB 安全性质的产物;灾备镜像与冷备主机也要经过同一道验收。
  6. 发布完成要求每个正在承接流量的实例同时具备四项事实:安装了获准摘要对应的构建产物;进程在该产物可用以后启动;合法报表通过;不受支持的继承字段在对应 SQL 发出以前返回修复版本的 S_ERROR。结果随部署记录保存;整体运行时间和版本横幅都不能代替逐实例证明。

数据库模型变化并非这项补丁的核心,发布计划仍要在迁移流量前验证 ReqDB 兼容性。让候选版本针对恢复后的合成数据库或脱敏快照运行,确认查询计划和继续升级流程,并区分应用回滚与数据库回滚。如果更大版本还包含数据迁移,修复后的 RequestDB 产物必须贯穿整个过渡阶段,不能让运营回退时只能选择脆弱镜像。

探针开始以前先定义中止条件:报表路径出现任何解释器调用;被拒字段仍发出 SQL;已加载版本与获准版本不一致;报表记录数量异常;或延迟出现实质性回退。操作人员应提前知道要捕获哪些证据、如何把实例移出流量。写好的中止流程会缩短暴露,也能防止发布压力把异常信号变成一条没有记录的例外。

发布关卡必需证据失败处理
构建产物修复分支版本或经过评审的回移补丁、源码提交、构建身份与不可变摘要阻止晋级
已加载代码三个辅助函数存在、八处 Web 查询求值消失、进程从获准产物启动移出流量并重启工作进程
行为合法用例矩阵通过,继承与未知选择在对应 SQL 发出前失败实例不得进入路由
实例群每个池成员与备用实例都有独立版本和测试证据继续逐实例记账
回滚备用产物保持相同安全性质,并拥有自己的摘要禁用脆弱回滚版本

7.5 复核历史,再依据证据决定凭证轮换范围

清理系统以前先保全证据。导出相关网关、DISET、RequestManager、数据库、身份提供方、主机、容器与网络记录,注明时区、保留窗口和采集哈希;出现可疑活动或无法解释的旧工作进程时,保存易失的进程与网络状态。匆忙删除日志,会毁掉用于判断哪些凭证需要紧急轮换的关联线索。

在每个受影响时段内,搜索各获准身份发出的明细、计数与去重候选值调用,优先复核未知或继承字段、类似表达式的标点、重复错误、异常时序,以及平时很少使用报表的调用者;随后把这些事件与服务异常、SQL 指纹、子进程、文件访问、配置读取、出站流量、队列变化、认证变更和日志缺口关联。

假设必须允许被证伪。一种解释可能认为奇怪字段来自旧门户,并且没有对应的进程活动;另一种解释则认为已认证调用之后,同一服务主机出现了新子进程与出站连接。为每种解释写出应当出现的证据,逐一检验,并记录仍无法取得的事实。单独一条可疑字符串,远弱于时间和身份都能对齐的“请求到进程”序列。

证据若表明代码曾在服务进程中运行,应隔离受影响实例,保存内存状态和磁盘证据,再用可信构建产物重建;同时轮换 DIRAC 配置秘密、数据库凭证、已存储或委派的代理凭证与令牌、自动化凭证,以及进程实际能够读取的下游材料。轮换从可信环境执行,旧值必须失效,并验证所有使用方已采用新值。

日志不完整、但高价值凭证在整个受影响时段都对进程可读时,预防性轮换仍可能合理;这项决定应记录为风险处置,不能写成凭证已被窃取的证明。反过来,也不能只因公告是“严重”,就判定组织内所有凭证都已泄露。范围要与服务账号的实际权限、挂载、配置和观测到的行为对齐。

通知与结案决定要分别标明已知事实、推断和未决问题。公告没有声称某个具体部署遭到利用,本地源码复核也回答不了这个问题。可靠的事件记录会说明搜索了哪些对象、遥测保留多久、发现什么迹象、执行什么处置,以及剩余不确定性为何可以接受,或仍由哪位负责人继续处理。

历史搜索还要考虑记录形态的变化。网关可能记录经过 JSON 转义的键名,DISET 会序列化 Python 值,应用消息也许会截断字段,安全产品还可能规范化下划线或标点。让合成流量经过每个仍有记录的层次,做出搜索样本并比较最终形态,再同时检索准确的安全标记和结构异常。盲区必须写明,不能让“未检出”的结论超出日志实际支撑范围。

7.6 凭证轮换跟随进程可达性,不能只跟着严重度标题走

公告把配置、数据库密码、已存储代理凭证和令牌列为 DIRAC 服务可能接触的材料示例。这些例子足以说明潜在影响很高,却不是每个安装环境的资产清单。本地决定要从 RequestManager 运行身份、挂载和配置、凭证代理、文件或内存使用方式,以及下游信任关系中建立。案件记录不要复制秘密原值,应使用稳定的凭证标识和位置,使调查人员能够讨论可达性、签发替代材料和审计旧值使用,同时不扩散正在保护的内容。

每项凭证单独占一行,记录运营负责人、签发方、存储位置、服务身份能否读取或申请、受影响时段内是否存在、内存与磁盘缓存方式、权限、寿命、下游使用方、历史审计来源、撤销办法和预期中断成本;再附上可能接触它的构建产物与工作进程人群。只挂载在一只中央 RequestManager 上的数据库密码,与许多任务按需取得的委派代理凭证是不同决定,两者又不同于从未进入服务进程的门户后端证书。

四种决定状态能让响应保持适当且可复核。若可疑执行附近已经观察到凭证访问或使用,应紧急撤销并替换;材料在受影响时段可读、遥测却无法判断是否访问,通常应有记录地预防性轮换;能够证明服务身份无法触及的材料,无需因本 CVE 轮换,但要保留可达性证据;可达性未知则分配负责人和期限,再按凭证价值、权限、寿命、信任范围及等待成本确定轮换优先级,不能悄悄把未知当成安全或已经泄露。

轮换必须从干净的管理环境执行,不能在完整性存疑的 RequestManager 主机上操作。先确认签发控制和紧急联系人,生成替代材料,经常规受保护通道分发,更新一项受控使用方,并观察认证失败、队列延迟与服务健康;随后扩展到其余使用方,撤销旧值,再通过负向检查证明旧认证确实失败。若证据支持代码执行,受影响工作进程要先从可信产物重建,再接收新秘密;在原进程上打补丁并直接交给它替代凭证,不能称为恢复。

数据库凭证会与连接池和事务行为相互作用。先列出使用该角色的每只 RequestManager 和管理程序,建立或轮换到最小权限替代值,更新连接配置,按驱动要求排空或重启连接池,并监控失败登录与队列处理。预定使用方全部迁移后才撤销旧密码,再在数据库审计中搜索截止点后的旧身份或旧凭证使用。如果数据库角色本身积累了不必要授权,就把权限收紧作为另一项授权变更,分别测试报表与请求处理行为。

证书、委派代理凭证和令牌遵循的是签发与失效语义,不是数据库连接池语义。记录主体、受众、作用域、有效期、委派链、刷新机制,以及接受它们的每个信任库或服务;在签发方或依赖方撤销、拒绝旧标识,更新自动化与门户服务,同时测试一条获准新流程和一条被拒旧流程。较短自然有效期可以减少剩余时间,却不能证明复制出的令牌从未使用;受影响时段能得出什么结论,仍取决于历史签发与使用日志。

配置秘密和自动化材料经常留存在运行容器以外。按照标识和版本检查编排系统秘密、部署变量、配置服务、备份快照、冷备、灾备系统、开发支持包与回滚镜像;更新未来工作进程真正会从中启动的来源,不能只替换当前挂载。主实例群可以通过全部固定代码测试,稍后仍可能从携带旧凭证的脆弱镜像恢复上线,除非构建产物与秘密基线一起前移。

只有处理完缓存生命周期,撤销才真正可观测。找出仍可能提交旧材料的内存客户端、本地文件、边车和代理缓存,通过受控重启或缓存失效清除,再监控退役标识的尝试。被拒请求要保留,因为它会暴露漏掉的使用方,但不能为了让告警安静就恢复旧值。可用性回退应限制在修复代码和另行获准的凭证路径上;同时重开脆弱源码与撤销凭证的回退,会让事件进一步扩大。

每一行最终用四项事实关闭:替代材料从可信环境签发;预定使用方已经证明采用;旧值撤销或过期到不再接受;截止点后的监控没有发现被接受的旧值使用,或出现的例外已完成调查。剩余缺口和支撑日志到期日期一并记录。“轮换工单完成”是行政状态,不等于这份技术证明。凭证可读、却没有事件证据时,把动作写成预防性风险处置;观察到访问或滥用时,则保留更强的事件结论和支撑链。

工程与事件负责人签署的是两种不同结论。工程方确认修复产物和缩小后的进程能力正在提供服务;事件方记录受影响时段、检索来源、凭证决定、通知义务、未决证据缺口和重新打开条件。可以由同一人协调两条工作线,但一份签字不能替代另一份记录缺少的事实。这样的分工让最后一章可以诚实收束:类型化报表字段防止代码层复发,进程和凭证材料则解释修复以前那段时间究竟做过什么。

手绘发布柜把构建产物送入分支负载均衡器:上方三只已验收服务进程带着启动时钟、构建产物封印和证据标签,下方未贴标签的旧进程仍等待替换。
图 7|全量验收以进程为单位:上方三只进程都有时钟、产物封印和证据标签;下方未贴标签的进程表示仍待摘除和替换的旧实例。每只替换进程都在同一修复基线上通过合法与拒绝检查以后,流量才恢复。

8 让 Python 永久退出未来的报表语法

8.1 让最终查询层只接收已经完成的决定,并用可复现证据结案

可复用的教训并不是禁止动态报表。用户依旧可以在运行时选择字段、过滤条件和排序,程序则先把请求翻译成一份很小的类型化表示:模型来自固定集合,列来自 ORM 元数据与产品策略的交集,比较和排序来自有限枚举,值始终保留为数据。数据库适配层只消费这份表示,不接收任意表达式;新增别名或运算时,允许用例、拒绝用例、序列化兼容与结果基数检查要随同提交。

授权仍然要留在设计里,却不能被要求兼任输入解析器。调用者可能有权查看 Request 报表,却无权读取本地扩展暴露的所属者、错误信息或凭证相关字段。先以映射列检查排除 Python 类的宽泛属性,再与该身份、入口和用途对应的字段策略取交集;解析结果与授权结果都应进入结构化日志,并由互相独立的测试证明。

开场那张报表卡终于走完旅程:Status 解析为 Request 的映射列,Type 触发程序预先设计的 Operation 连接,比较值保持绑定数据,排序方向落在服务掌握的两项操作之一。合法卡片通过打开的闸门进入 SQLAlchemy;继承属性在映射器的 column_attrs 成员检查处被拒绝。没有哪张桌子再猜字符串可能表达什么,每次接受或拒绝都能由另一位运营者从构建产物、进程与日志中复现。

最终结论因此可以精确落笔:官方资料证明严重的认证后代码执行路径和修复后的 ORM 映射列检查;逐实例发布证据可以证明实例群正在运行这项检查;历史遥测则可能支持“发现可疑活动”“在明确范围内未见迹象”或“仍无法判定”三种不同结论。代码所有者维护有限词汇,发布工程师维护产物与进程证明,运行团队复核版本和拒绝信号,事件团队维护日志保留与调查结论。下一张普通报表卡抵达最后一张桌子时,只剩产品允许的决定,不再意外获得一门编程语言。

暖纸手绘事件室把请求账本、进程遥测、凭证保险柜、不可变构建产物架和签字结案夹放在同一张长证据桌上。
图 8|结案把工程证明与调查范围连接起来:一侧是修复产物与线上进程,另一侧是历史搜索、凭证决定和明确标注的有限不确定性。

研究记录

9证据、对象与来源

下面保留本文实际使用的标识、时间和原始材料,便于继续调查。

9.1研究对象

报告涉及的产品、行为者、技术、受影响对象和控制点。

CVECVE-2026-45579

DIRAC RequestManager Web 报表中的已认证 Python 求值

公告GHSA-9jpv-c7p4-997x

DIRAC 于 2026 年 7 月 13 日发布的官方安全公告

CVSS 评分9.9 严重 / CWE-95

官方 CVSS 3.1 向量 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

修复版本8.0.79 / 9.0.22 / 9.1.10

公告列出的三个维护分支修复点

受影响方法getRequestSummaryWeb / getRequestCountersWeb / getDistinctValues

v9.1.10 同时修复的三条 RequestDB 报表路径

拒绝信号Unknown Request attribute

修复版本拒绝未知或继承属性时的预期错误

控制点_get_column / inspect(model).column_attrs

补丁新增的 ORM 映射列检查

9.2事件时间

  1. 全局公告列出三段不连续约束

    >= 6, < 8.0.79 、 >= 8.1.0a1, < 9.0.22 与 >= 9.1.0, < 9.1.10 受影响;各段上界就是对应首个修复版本。

  2. 三个修复版本均已发布

    DIRAC 于 6 月 1 日发布 8.0.79,并于 6 月 4 日发布 9.0.22 与 9.1.10。

  3. DIRAC 发布 GHSA-9jpv-c7p4-997x

    公开记录给出严重等级、CVSS 9.9 和已认证服务器端命令执行影响。

  4. SOSEC 完成修复源码复盘

    SOSEC 交叉核对官方标签引用及其解析到的提交号、处理器入口、八处 Web 查询求值点、替代辅助函数与回归测试。

9.3来源与材料

  1. DIRAC GHSA-9jpv-c7p4-997x 安全公告https://github.com/DIRACGrid/DIRAC/security/advisories/GHSA-9jpv-c7p4-997x
  2. GitHub Advisory Database 不连续受影响范围记录https://github.com/advisories/GHSA-9jpv-c7p4-997x
  3. DIRAC v9.1.9 至 v9.1.10 官方源码比较https://github.com/DIRACGrid/DIRAC/compare/v9.1.9...v9.1.10
  4. v9.1.9 受影响 ReqManagerHandlerhttps://github.com/DIRACGrid/DIRAC/blob/v9.1.9/src/DIRAC/RequestManagementSystem/Service/ReqManagerHandler.py
  5. v9.1.9 受影响 RequestDBhttps://github.com/DIRACGrid/DIRAC/blob/v9.1.9/src/DIRAC/RequestManagementSystem/DB/RequestDB.py
  6. v9.1.10 修复后的 RequestDBhttps://github.com/DIRACGrid/DIRAC/blob/v9.1.10/src/DIRAC/RequestManagementSystem/DB/RequestDB.py
  7. v9.1.10 RequestDB 回归测试https://github.com/DIRACGrid/DIRAC/blob/v9.1.10/src/DIRAC/RequestManagementSystem/DB/test/Test_RequestDB.py
  8. DIRAC 官方发布与标签记录https://github.com/DIRACGrid/DIRAC/releases
  9. DIRAC Request Management System 文档https://dirac.readthedocs.io/en/latest/DeveloperGuide/Systems/RequestManagement/index.html
  10. DIRAC ReqManager 与授权配置示例https://dirac.readthedocs.io/en/latest/DeveloperGuide/Systems/RequestManagement/index.html#reqmanager-and-reqproxies
  11. SQLAlchemy 映射属性检查说明https://docs.sqlalchemy.org/en/20/faq/ormconfiguration.html#how-do-i-get-a-list-of-all-columns-relationships-mapped-attributes-etc-given-a-mapped-class
  12. SQLAlchemy 运行时检查 APIhttps://docs.sqlalchemy.org/en/20/core/inspection.html
  13. SQLAlchemy Mapper.column_attrs APIhttps://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.Mapper.column_attrs
  14. Python eval 文档与不可信输入警告https://docs.python.org/3/library/functions.html#eval
  15. Python 数据模型与属性查找https://docs.python.org/3/reference/datamodel.html
  16. Python 上下文日志指南https://docs.python.org/3/howto/logging-cookbook.html#adding-contextual-information-to-your-logging-output