P1 security batch: quota execution checks, cost accounting, goal/coordinator semantics, PR channel, orphan signature, control-plane and console hardening

- Docker quota watchdog + fail-closed quota checks, system tasks need real owner
- max_cost_usd actually accumulated, cancel records cost, estimated rows excluded from unpriced cap, cache_write declared non-billed
- goal-resume keeps original verify on breakpoint resume, goal retry falls back to resume, goal_max_rounds capped at 100 at create and execution
- coordinator children inherit parent model/effort, orphans marked interrupted with audit
- PR jobs on docker workers return explicit needs_local_worker instead of silent empty
- worker code signature covers 6 files and is checked before claim, fail-closed
- sub2api lazy provisioning with rotation, backup whitelist + 0600, push SSRF block, repos/workers desensitized, key/project audits and limits, console buttons/quota/status wording, coordinator/automation quota reserve
This commit is contained in:
host committed 2026-10-27 16:27:50 +00:00
1 parent d254240d84
commit 1402dc67fd
20 files changed
+2616 -179

No files matched your search

+125 -5
View File
@@ -37,15 +37,22 @@
发送端是后续待接线的工作,见模块末尾的缺口说明。
- 本模块不跑真实模型回合。webhook/slack 的真实发送只 POST
到订阅者自己填的 target URL(创建订阅已要求项目
create_task 权限;能建订阅的成员本来就能经任务执行代码,
因此不对 target 做内网屏蔽——屏蔽既拦不住提权,也会误伤
本地接收器)。密钥、token、证书一律不由本模块读取、写入
或返回。
create_task 权限)。P1 收紧后:真实发送层对 target 做 SSRF
校验——loopback、私有网段、链路本地、云元数据地址一律不
POST;主机名先 DNS 解析再按解析出的 IP 判,解析失败拒绝
(见 validate_push_target)。本地接收器如需放行,须运维显式
置 CODEX_PUSH_ALLOW_PRIVATE_TARGETS=1(默认关闭,测试用
本地接收器也须经此开关,不在代码里默认放行 loopback)。
密钥、token、证书一律不由本模块读取、写入或返回。
"""
import ipaddress
import json
import os
import socket
import sqlite3
import urllib.error
import urllib.parse
import urllib.request
from cloud.db import connect as _db_connect
from cloud.db import PgConnection as _PgConnection
@@ -67,6 +74,7 @@ __all__ = [
"send_webhook",
"send_slack",
"dispatch_event",
"validate_push_target",
]
CHANNELS = ("webhook", "slack", "webpush", "apns", "fcm")
@@ -311,6 +319,91 @@ def _require_http_url(target, channel):
return target
# 云元数据主机名:即使解析出的 IP 恰好不在私有段,也直接挡。
_BLOCKED_HOSTNAMES = frozenset({"metadata.google.internal"})
def _allow_private_targets():
"""运维显式开关:仅 CODEX_PUSH_ALLOW_PRIVATE_TARGETS=1 时放行
内网目标(本地接收器场景)。默认关闭;开关只影响 IP 段判定,
不影响 scheme 校验。"""
return os.environ.get(
"CODEX_PUSH_ALLOW_PRIVATE_TARGETS", "").strip() == "1"
def _default_resolver(hostname):
"""默认 DNS 解析:返回 IP 字符串列表;解析失败抛异常,
由 validate_push_target 转成“拒绝”。"""
infos = socket.getaddrinfo(hostname, None)
out = []
for info in infos:
sockaddr = info[4] if len(info) > 4 else None
if sockaddr and sockaddr[0]:
out.append(str(sockaddr[0]))
return out
def _ip_is_blocked(ip_text):
"""非全局单播 IP 一律算内网/特殊地址:loopback、私有网段、
链路本地(169.254/16,含云元数据 169.254.169.254)、保留段、
组播、未指定地址都在 is_global=False 里。"""
try:
return not ipaddress.ip_address(ip_text).is_global
except ValueError:
return True
def validate_push_target(target, resolver=None, check_dns=True):
"""SSRF 校验:通过返回 None,否则返回原因字符串(不抛)。
顺序:scheme(http/https)-> 主机名存在 -> 元数据主机名 ->
IP 字面量直接判段 -> (check_dns 时)DNS 解析后逐 IP 判段,
解析失败或解析出空结果都拒绝(DNS 重绑定下解析时与发送时
仍可能不一致,这是本层能做的上限,见残留说明)。
check_dns=False 时只做字面量判定:供直接调 send_* 但不想
做 DNS 的调用方用;dispatch 的真实发送路径一律
check_dns=True,显式 sender 路径不走本预检(见 dispatch)。
"""
if not isinstance(target, str) or not (
target.startswith("http://") or target.startswith("https://")):
return "target must be an http(s) URL"
try:
host = urllib.parse.urlparse(target).hostname
except ValueError:
return "target host is invalid"
if not host:
return "target host required"
lowered = host.lower()
if lowered in _BLOCKED_HOSTNAMES or lowered.endswith(
".metadata.google.internal"):
return "target host is a cloud metadata address"
if _allow_private_targets():
return None
try:
ipaddress.ip_address(host)
is_literal = True
except ValueError:
is_literal = False
if is_literal:
if _ip_is_blocked(host):
return "target resolves to a non-public address: %s" % host
return None
if not check_dns:
return None
resolve = resolver or _default_resolver
try:
ips = resolve(host)
except Exception:
return "DNS resolution failed for target host"
if not ips:
return "DNS resolution failed for target host"
for ip_text in ips:
if _ip_is_blocked(ip_text):
return ("target host resolves to a non-public address: %s"
% ip_text)
return None
def send_webhook(message):
"""webhook 通道真实发送:POST JSON 到订阅的 target URL。
@@ -319,6 +412,11 @@ def send_webhook(message):
RuntimeError(dispatch 记入 errors,不算 sent)。
"""
target = _require_http_url(message["target"], "webhook")
blocked = validate_push_target(
target, resolver=message.get("_resolver"))
if blocked is not None:
raise RuntimeError(
"webhook delivery failed: target blocked: %s" % blocked)
payload = message["payload"] if isinstance(
message["payload"], dict) else {}
ok, detail = _post_json(target, {
@@ -340,6 +438,11 @@ def send_slack(message):
事件附 error 摘要(截断 500 字)。非 2xx 抛 RuntimeError。
"""
target = _require_http_url(message["target"], "slack")
blocked = validate_push_target(
target, resolver=message.get("_resolver"))
if blocked is not None:
raise RuntimeError(
"slack delivery failed: target blocked: %s" % blocked)
payload = message["payload"] if isinstance(
message["payload"], dict) else {}
text = "[codex] task %s %s (project %s)" % (
@@ -357,7 +460,8 @@ def send_slack(message):
DEFAULT_SENDERS = {"webhook": send_webhook, "slack": send_slack}
def dispatch_event(db, event_type, project_id, payload, sender=None):
def dispatch_event(db, event_type, project_id, payload, sender=None,
resolver=None):
"""分发一次状态变化事件,只发本项目的订阅。
参数:
@@ -418,6 +522,7 @@ def dispatch_event(db, event_type, project_id, payload, sender=None):
}]
return result
real_send = sender is None
if sender is None:
# 内置真实发送端:webhook/slack 真 POST;显式 sender
# 传入时不走这里(注入优先),webpush/apns/fcm 永远
@@ -445,6 +550,21 @@ def dispatch_event(db, event_type, project_id, payload, sender=None):
result["errors"].append(
dict(base, reason=MISSING_CREDENTIAL_REASONS[sub["channel"]]))
continue
# SSRF 预检(真实发送路径,发送前挡,不 POST):含 DNS
# 的全量校验。显式 sender 路径不预检:注入的 sender 是
# 调用方自控的投递(测试 seam),不是服务端发起的 HTTP,
# 挡它不增加生产安全性;生产服务端一律走 sender=None
# 的真实发送,必经本预检与 send_webhook/send_slack 的
# 二次校验。原因文案含 "delivery failed",与旧调用方
# 对投递失败的断言兼容。
if real_send:
blocked = validate_push_target(
sub["target"], resolver=resolver, check_dns=True)
if blocked is not None:
result["errors"].append(
dict(base, reason="%s delivery failed: target blocked:"
" %s" % (sub["channel"], blocked)))
continue
# webhook/slack:sender=None 时已换成内置真实发送端;
# 显式 sender 里没有该通道时明确报错,不静默成功。
channel_sender = _resolve_channel_sender(sender, sub["channel"])