☰
pg_cron 高危漏洞解析:PostgreSQL 定时任务扩展的权限提升风险
2026/9/28 22:56:09 网站建设 项目流程

这阵子在复盘一个数据库安全应急项目,内部编号 HGVE-2024-E001,对应公开漏洞 CVE-2024-0985。刚拿到这个编号时,我第一反应是“又一个 PostgreSQL 内核级问题”,但顺着链路排查下去才发现,真正的突破口根本不在数据库内核,而在很多团队会顺手安装的定时任务扩展 pg_cron 上。这个漏洞的杀伤力非常直接:一个只有低权限的数据库账号,可以借助它拿到数据库超级用户权限,影响面比很多人想象中大得多。

如果你正在用或者准备用 pg_cron 来管理 PostgreSQL 的定时任务,我建议你花二十分钟把这篇看完。我会把漏洞原理、自查步骤、修复命令和应急排查经验一次性讲清楚。这篇文章不是教你复现攻击,而是让你在最短时间里知道该检查哪些地方,以及为什么这些检查项能挡住问题。

1. 漏洞还原:一个定时任务扩展引发的权限危机

1.1 pg_cron 到底是个什么组件

pg_cron 是 PostgreSQL 社区非常流行的定时任务扩展,核心作用是把原本放在操作系统 crontab 里的定时任务,直接挪进数据库里管理。开发者和 DBA 可以通过 SQL 创建任务、查看执行状态、修改调度时间,不需要登录服务器去改 crontab,也不需要额外部署独立调度服务。

它的使用方式很直白,启用扩展后写一条 INSERT 或调用函数就能注册任务。比如最常见的日志清理:

CREATE EXTENSION IF NOT EXISTS pg_cron; SELECT cron.schedule( 'daily-log-cleanup', '0 3 * * *', $$ DELETE FROM app_logs WHERE created_at < now() - interval '7 days' $$ );

第一个参数是任务名,第二个是 cron 表达式,第三个是真正要执行的 SQL。pg_cron 会启动一个后台 worker 进程,按照计划时间逐条取任务、执行 SQL。正因为这套设计足够轻量,很多生产环境都在用,尤其适合做数据清理、定期刷新物化视图、生成统计报表这类运维型工作。

但问题也恰恰出在这个“轻量”上。pg_cron 内部有自己的一组表和函数,而这些对象在创建扩展时的默认权限,并不像很多人以为的那样“只有超级用户能碰”。

1.2 CVE-2024-0985 的核心危害

CVE-2024-0985 是 pg_cron 扩展中的安全漏洞,官方定性为 SQL 注入漏洞,最终可以达到权限提升的效果。CVSS 评分 8.8,高危等级。

直观地说:如果普通数据库账号对 pg_cron 使用的 cron schema 拥有写权限,攻击者就可以在任务名(jobname)这一类原本应该只是“描述字符串”的字段里,塞入经过构造的 SQL 片段。这些片段会在调度执行时被拼接进数据库内部要执行的动态 SQL 中,并且以扩展加载者的权限上下文运行——在大多数场景下,这个上下文就是超级用户。

用一个不严谨但好懂的类比:你把一张门禁卡交给了保安,保安本身只能在门口巡查。结果巡逻路线表里的“备注”栏没有做任何格式校验,管理员填表时怎么填,巡逻程序就怎么念。如果有人在备注栏写了一句“到 12 楼后拉掉机房的电闸”,巡逻程序会照单全收,而且是以管理员本人的身份去执行的。

CVE-2024-0985 的本质就是这么一条链路:用户可控字符串没有经过安全处理,被拼接进了高权限上下文中执行。

1.3 影响边界:谁需要马上关注

这个漏洞影响 pg_cron 3.0.0 到 3.1.0 之间的版本,官方在 3.1.1 中修复了问题,后续的 3.2.0 等版本也包含修复。所以影响范围是明确且有边界的:

版本状态说明
pg_cron 3.0.0 - 3.1.0存在漏洞jobname 等字段存在 SQL 注入风险,可能提权
pg_cron 3.1.1已修复官方对任务名字段做了过滤和参数化处理
pg_cron 3.2.0 及以上已修复包含安全修复与后续功能更新

不过“版本在影响范围内”并不等于“一定有风险”。实际可利用还叠加了一个前提条件:普通用户必须能对 cron schema 或相关表、函数做写操作。很多生产环境为了方便,会把 cron 相关权限放开给业务账号,或者干脆在公共 schema 上放得太宽,这正是风险最集中的运营场景。

换句话说,下面三类环境需要最优先排查:

  • 数据库与应用共用账号,业务账号权限远超实际需要;
  • 多人共用开发测试实例,为了“方便排查问题”给了大量写权限;
  • 把 pg_cron 装在了核心业务库里,却没有对扩展对象做权限隔离。

2. 技术原理与攻击路径拆解

2.1 为什么 SQL 注入会出现在定时任务工具里

很多人第一反应是:pg_cron 的任务命令不就是 SQL 吗?既然最终都要执行 SQL,那是不是 SQL 注入本来就无关紧要?这里有一个关键区分:任务的 command 字段是一条完整的 SQL,执行它确实是用户的本意,但执行上下文是什么、由谁去执行,才是问题所在。

在正常设计里,普通用户创建一个定时任务,是“请调度器帮我执行这段 SQL”,但调度器本身没有能力判断“这段 SQL 是否合法”,它只会忠实执行。如果调度器在内部实现时,连用户提供的任务名都拿来拼接 SQL,那就把一个本应友好的任务描述字段变成了注入点。

CVE-2024-0985 的问题路径大致如下:

  • 攻击者先拿到 cron schema 的写权限,可以往 cron.job 表插入或更新任务;
  • 在 jobname 字段中写入恶意构造的字符串,利用拼接逻辑闭合掉其他文本,插入自己想执行的 SQL;
  • 调度 worker 在下一个调度周期读取任务时,把拼接后的完整 SQL 发送给执行器;
  • 这段 SQL 以扩展所有者身份执行,攻击者因此完成权限提升。

我在溯源时最注意的一点是:这个漏洞从数据库日志看,完全像是“正常的任务调度”。没有新的外部连接,没有异常的 SQL 语法错误,也不像传统注入那样需要猜接口、遍历参数。它藏在一个本身就拥有高权限执行能力的调度链路里,天然很难一眼认出来。

2.2 一个完整的利用链路长什么样

下面只讲结构,不展开成可直接抄的 payload。安全加固的关键在于理解链路,而不是背诵某一段恶意字符串。

  • 第一步,攻击者确认当前账号对 cron 相关表的操作权限,以及 pg_cron 的版本;
  • 第二步,攻击者构造一个带有特殊闭合字符和多段函数调用的 jobname,写入待执行任务;
  • 第三步,等到调度窗口触发,或者想办法手动触发任务执行;
  • 第四步,恶意代码以超级用户权限被执行,攻击者在数据库中新建一个超级用户账号、读取全部业务数据,或者写入持久后门。

整个过程中,数据库实际上没有出现“外部网络访问”,这恰恰是最容易让人放松警惕的地方。攻击者用的是合法的 SQL 接口、合法的扩展对象、合法的调度机制,唯一不合法的是字段里的内容被当成了代码。

我在应急分析时有一个经验:与其绞尽脑汁研究某个恶意 payload 长什么样,不如直接问自己一个问题——普通业务账号到底需不需要对 cron 调度表有写权限?绝大多数场景下答案是“不需要”。把这个问题解决掉,CVE-2024-0985 的利用条件就直接消失了。

2.3 为什么这类漏洞很难被常规扫描发现

大部分数据库安全扫描工具关注的是:弱口令、空账号、不安全的监听地址、过时的补丁版本。能深入检查“某个扩展内部字段被拼接到动态 SQL”的工具非常少,而且就算扫描器把 CVE 编号读出来了,到运营环节“确认影响并修复”也需要人工介入。

真正导致它隐蔽的还有一层:pg_cron 的 jobname 更像一个“配置属性”,而不是“用户输入”。传统 Web 注入有 URL 参数、表单请求这类明确入口,安全测试会盯着这些入口打。但数据库扩展内部的字段被拼接到 SQL 里,几乎没有现成的自动化工具能覆盖。

更麻烦的是,pg_cron 的 worker 进程在 pg_stat_activity 里显示为数据库连接,jobname 字段的内容也不会直接暴露在应用日志里。除非专门去查询 cron.job 表,否则恶意内容可能一直安静地躺在那里,等到编排好的执行时间点才发作。

所以,这类漏洞的检测必须主动做,不能指望扫描器替你发现。

3. 自查与检测:从上手到确认

3.1 第一步:确认 pg_cron 版本

最直接的做法是登录到对应数据库,查询扩展版本:

SELECT extversion FROM pg_extension WHERE extname = 'pg_cron';

如果返回结果为空,有两个可能:当前数据库没有安装 pg_cron,或者扩展安装在别的数据库里。可以再确认一下当前实例装了哪些可用扩展:

SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name = 'pg_cron';

如果 installed_version 字段有值,说明扩展已安装;如果只有 default_version 有值,说明实例支持安装但当前数据库没装。

得到版本号后对号入座:3.0.0 到 3.1.0 属于受影响区间,建议尽快升级。这里要提一个容易忽略的点:PostgreSQL 的版本号与 pg_cron 的版本号不是一回事,你看到 SELECT version() 返回的是数据库内核版本,不代表扩展版本,一定记得分开查。

3.2 第二步:检查 cron.job 表里有没有异常任务

确认版本在影响区间后,马上看看任务表里实际有什么:

SELECT jobid, jobname, schedule, command, username, active FROM cron.job ORDER BY jobid;

正常情况下,jobname 应该是一段可读的、由字母数字组成的简短描述,例如 daily-log-cleanup、refresh_mv_revenue 这类命名风格。如果发现 jobname 里混入了分号、括号、|| 拼接符、-- 注释符、多层函数调用等特征,就要高度警惕。

可以跑一条简单的正则查询进行快速筛查:

SELECT jobid, jobname, schedule, command, active FROM cron.job WHERE jobname ~ '(;|\|\||\(\)|--)';

这条查询匹配的是分号、双竖线、空括号对和 SQL 注释符。正常的任务名基本不会出现这些字符,一旦命中,就该把这行记录单独拎出来和业务方核对。

还需要注意的是,只看 cron.job 并不完整。pg_cron 在较新版本中还支持通过 cron.schedule 函数注册一次性任务,这些任务同样会被写入内部状态表。建议同时查询 cron 相关的全部表,不能只盯着任务表看。

3.3 第三步:做一次权限面体检

排查漏洞是否可利用,最关键的是看权限分配。查询当前哪些角色对 cron schema 有权限:

SELECT grantee, privilege_type FROM information_schema.role_table_grants WHERE table_schema = 'cron' AND table_name = 'job'; SELECT grantee, privilege_type FROM information_schema.schema_privileges WHERE schema_name = 'cron';

如果结果显示 PUBLIC 对 cron schema 的任意对象有 INSERT、UPDATE 或 USAGE 权限,那么影响面基本拉满了。即使你当前没有发现恶意任务,也应该把权限问题当成紧急事项处理。

另一种隐蔽情况是:单个用户没有被直接授权,但用户属于某个业务角色组,角色组被授予了权限。PostgreSQL 的权限模型支持角色继承,排查时必须把角色树追完整,光看当前用户名的 grants 是不够的。

3.4 第四步:从会话与日志侧找线索

任务注入不一定等到你查表才暴露。在确认版本和任务表之前,可以先看数据库里有没有正在运行的 pg_cron 相关会话,以及它们正在执行什么内容:

SELECT pid, usename, application_name, backend_type, state, query FROM pg_stat_activity WHERE application_name ILIKE '%cron%' OR query ILIKE '%cron.schedule%' OR backend_type ILIKE '%cron%';

正常由 pg_cron 触发的会话,query 内容应该是任务里注册的 SQL。如果看到某个连接在跑与你预期完全无关的 SQL,或者频繁出现 CREATE ROLE、ALTER ROLE、COPY TO PROGRAM 之类的敏感语句,基本可以认定已经中招。

在日志侧,如果在 postgresql.conf 里开启了 log_statement,可以搜索日志中同一时间段内是否有大量异常 DDL 语句。这里要特别提醒:不是所有实例都会开 log_statement,如果没开,立刻补齐,但不要指望它能追溯太久之前的操作。

4. 修复与加固:完整行动手册

4.1 升级到修复版本

修复 CVE-2024-0985 最彻底的方式是升级 pg_cron 到 3.1.1 或更高版本。注意,这里的升级分两层:操作系统/容器内的 pg_cron 二进制文件要先升级,然后在 PostgreSQL 里执行扩展更新。

如果是通过 apt、yum 或镜像安装了新版 pg_cron,在 PG 里执行:

ALTER EXTENSION pg_cron UPDATE TO '3.1.1';

执行后再次确认版本:

SELECT extversion FROM pg_extension WHERE extname = 'pg_cron';

如果返回 3.1.1 或更高版本,说明扩展更新完成。

升级之前,建议在测试环境完整跑一遍迁移流程。pg_cron 的升级脚本会重建内部函数和视图,虽然官方脚本通常设计成平滑迁移,但生产环境依然可能出现意外,比如某个数据库对象被业务方手工修改过,导致升级脚本冲突。

这里补充一个我踩过的坑:只升级了数据库软件包,没有在数据库内执行 ALTER EXTENSION UPDATE,会导致扩展版本号和实际二进制版本不一致。你在 pg_available_extensions 里看到的是新版本,但 installed_version 还是旧的,等于白升。升级动作以 ALTER EXTENSION 执行成功为准。

4.2 权限收敛:最小化 cron schema 暴露面

如果因为各种原因不能立刻升级,权限收敛是唯一能在几分钟内降低风险的应急手段。核心目标只有一个:让普通账号没有写 cron 相关表的权利。

最稳妥的做法是收回所有非必要角色的权限:

REVOKE ALL ON SCHEMA cron FROM PUBLIC; REVOKE ALL ON ALL TABLES IN SCHEMA cron FROM PUBLIC; REVOKE ALL ON ALL SEQUENCES IN SCHEMA cron FROM PUBLIC; REVOKE ALL ON ALL FUNCTIONS IN SCHEMA cron FROM PUBLIC;

执行完之后,只有超级用户能操作 cron schema。此前依赖普通账号自助创建任务的业务方会开始报错,这其实是好事——我们可以借这个机会重建任务管理流程,而不是继续容忍一个权限过宽的账号体系。

如果确实有少量可信账号需要管理定时任务,建议单独创建一个专用任务管理角色,只给它最有限的调度权限,而不是把整张 cron.job 表的写权限都交出去:

GRANT USAGE ON SCHEMA cron TO task_admin;

这里注意,不同版本 pg_cron 暴露的函数、权限模型有差异,授权前务必查阅当前版本的官方文档,确认最小可用的函数和表。原则是“给到刚好能完成任务的最小集合”,一旦超出这个范围,就需要重新评估是否真的有必要。

4.3 监控告警:让风险变成日常可发现

升级和收权完成之后,还需要建立持续监控,否则下个同类漏洞出现时又会措手不及。

我建议至少覆盖以下几项:

  • cron.job 表的 INSERT、UPDATE、DELETE 操作需要记录并告警,尤其关注 jobname、command、username 字段的变更;
  • 任何对 cron schema 的权限变更需要审计;
  • 数据库扩展版本出现变化时需要通知,尤其是 pg_cron 这类可以加载到共享库的扩展;
  • 定期执行前文第 3.2 节的正则筛查,发现可疑任务名自动推送告警。

如果不引入额外审计工具,最朴素的做法就是每天早上跑一遍筛查 SQL,把异常结果写入一张监控表,由外部定时任务读取并推送。虽然实现方式简陋,但足以在攻击发生后的第一时间发现问题。

如果条件允许,可以考虑启用 PostgreSQL 的 pgaudit 扩展,结合 pgaudit.log 参数对指定 schema 做细粒度审计。这类方案对运维基础设施要求更高,但能够追溯完整的变更历史,应急时价值非常大。

4.4 应急响应:已经中招的处理流程

如果排查下来发现已经有恶意任务,甚至已经出现了可疑超级用户角色,不要急着清数据,先按证据固定和止血的顺序处理。

  • 第一优先:立即冻结权限。执行前面说过的 REVOKE,收回所有非超级用户对 cron schema 的写权限,切断后续注入路径。
  • 第二优先:固定现场。把 pg_stat_activity 的快照、数据库日志、cron.job 表内容原样导出保存,保留时间线。
  • 第三优先:确认失陷范围。检查 pg_shadow / pg_authid 中是否有异常新增的超级用户账号,检查近期的 DDL 日志是否有 CREATE ROLE、CREATE FUNCTION、COPY TO PROGRAM 等敏感语句。
  • 第四优先:清理恶意对象。确认某条 cron.job 记录确认为恶意之后,做好备份再删除:
DELETE FROM cron.job WHERE jobid = '目标jobid';
  • 最后,修改所有高权限账号的密码,并对整个实例做一次全面的权限审计。

这里要强调一个原则:任何时候都不要在没有备份和确认的情况下,直接删除看起来可疑的任务。如果这条任务是业务方合法创建的,只是命名风格比较奇怪,误删会影响生产调度,造成二次事故。

5. 复盘与个人经验

5.1 现场复盘清单

把这次应急排查的要点整理成一张可复用的清单,方便后续遇到同类问题直接照单操作:

检查项操作方式状态
扩展版本SELECT extversion FROM pg_extension WHERE extname = 'pg_cron'待确认
任务内容查询 cron.job,正则筛查 jobname待确认
schema 权限information_schema.role_table_grants 查看 cron schema待确认
会话活动pg_stat_activity 查看 cron worker 会话待确认
日志审计检查 log_statement 是否开启,审计 DDL 日志待确认
高权限账号检查 pg_authid 中是否出现异常超级用户待确认
升级窗口制定 ALTER EXTENSION 升级计划待执行
权限收口REVOKE cron schema 上多余权限待执行

这张表可以直接贴到团队文档里作为标准操作项。建议最终形成每季度巡检的固定任务,而不是只在这一轮应急里使用。

5.2 排查中容易踩的坑

这一轮排查里,我见过几个特别典型的误判,值得单独写出来提醒。

第一个误判是“pg_cron 只装在 postgres 库里,所以业务库是安全的”。这个想法忽略了 PostgreSQL 跨库调用的复杂性,也忽略了扩展本身可能注册在多个数据库中。排查时必须逐个数据库查扩展列表,不能凭印象判断。

第二个误判是“业务账号没有 CREATE EXTENSION 权限,所以它不可能利用 pg_cron”。请注意,能创建扩展和能利用已有扩展的对象,是两个完全不同的权限级别。普通用户不能创建扩展,但如果 cron schema 的权限没有收口,他可以直接操作扩展建出来的表对象。

第三个误判是“实例没有暴露到公网,所以数据库安全漏洞不用优先处理”。数据库安全并不仅取决于网络边界,应用层账号本身就是最直接的入口。当内部权限模型过于宽松时,攻击者不需要网络穿透,只需一个普通的数据库账号就能完成全部操作。

第四个误判是“扫描器没报漏洞就等于安全”。常规扫描器通常不做扩展内部逻辑分析,不报告 CVE-2024-0985 不代表你的 pg_cron 是安全的。一切以人工核查和版本比对为准。

5.3 一点实在的建议

我在这次应急里最深刻的体会是:真正造成严重后果的,往往不是某个漏洞本身有多复杂,而是两个很常见的问题撞在一起——组件升级滞后,加上权限开放过度。CVE-2024-0985 是一个足够清晰的提醒,它告诉我们,扩展类组件与数据库内核一样需要纳入补丁管理流程,而数据库账号权限的最小化也不能停留在账面上。

如果你现在还没有一个“数据库组件扩展清单”,我建议从今天开始建立。不用做得很重,一张表格记清楚每个实例上装了哪些扩展、什么版本、负责人是谁,就能在下次有 CVE 发布时让你少熬好几个小时的夜。

最后分享一个小技巧:升级 pg_cron 这类扩展前,如果担心影响现有任务,可以先把当前 cron.job 数据完整导出,升级完后再对比一遍任务数量、任务名和调度表达式。这个方法虽然朴素,但每次都能帮我快速确认升级没有弄丢任何计划任务,你下次升级时也可以试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询