Skip to content

feat: issue-642#706

Open
ceilf6 wants to merge 3 commits into
TencentCloud:masterfrom
ceilf6:feature/issue-642
Open

feat: issue-642#706
ceilf6 wants to merge 3 commits into
TencentCloud:masterfrom
ceilf6:feature/issue-642

Conversation

@ceilf6

@ceilf6 ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown

Summary

  • Add CubeAPI webhook delivery for sandbox lifecycle events (sandbox.created, sandbox.deleted, sandbox.paused, sandbox.resumed).
  • Support JSON/env endpoint configuration, per-event subscriptions, async delivery queue, retry with exponential backoff, and optional HMAC-SHA256 signatures.
  • Add runnable receiver example plus English/Chinese integration docs.

Validation

  • cargo fmt --manifest-path CubeAPI/Cargo.toml -- --check
  • cargo test --manifest-path CubeAPI/Cargo.toml
  • npm run docs:build from docs/
  • python3 -m py_compile examples/webhook-receiver/receiver.py
  • git diff --check

Autonomously-by: Codex:GPT-5

Autonomously-by: Codex:GPT-5
Copilot AI review requested due to automatic review settings July 2, 2026 02:59
@cubesandboxbot

cubesandboxbot Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review: PR #706 — Webhook Delivery for Sandbox Lifecycle Events

Overall, this is a well-structured implementation with thorough tests for happy paths, solid SSRF guards at the URL validation layer, and careful secret redaction. Below are noteworthy issues organized by severity.

🔴 High Severity

1. response_body_summary reads the body then discards it (CubeAPI/src/logging/http.rs:307-332)

The function reads up to 2048 bytes from the error response but returns <N response body bytes omitted> instead of including the actual text. Every webhook delivery failure log reads like webhook endpoint returned 500: <42 response body bytes omitted>, giving operators zero diagnostic information. Fix: use String::from_utf8_lossy(&body).

2. DNS rebinding bypasses startup-only IP validation (CubeAPI/src/config/mod.rs:303-348)

Hostnames are resolved to IPs and validated against the private-IP blocklist only at startup, but reqwest re-resolves at delivery time. An attacker controlling DNS for the webhook URL can serve a safe IP at startup and switch to a private/metadata IP by delivery time. Consider resolving+validating at delivery time, or pinning the resolved SocketAddr.

3. No replay protection on signed payloads (CubeAPI/src/logging/http.rs:334-340, receiver.py:25-31)

The HMAC covers only the request body. X-Cube-Delivery and the timestamp are not signed. An observer can replay the payload verbatim at any future time, and the example receiver has no delivery-ID tracking. Include the delivery ID in the HMAC input and recommend receivers enforce deduplication.

🟠 Medium Severity

4. Two-tier silent event drops with no backpressure (CubeAPI/src/logging/http.rs:129-136, 169-177)

Events are silently dropped at two levels: try_send in log() (channel full), and try_acquire_owned in the worker (endpoint concurrency saturated). An operator's only indication is a warn! log. At minimum, document both drop paths in the webhooks doc. Consider using send().await for backpressure, or acquire_owned().await instead of try_acquire_owned() in the worker so queued events aren't wasted at the semaphore gate.

5. Background worker panic silently kills all webhook delivery (CubeAPI/src/logging/http.rs:156-207)

The tokio::spawn handle returned by spawn_worker is discarded. If the background task panics (e.g., from an expect() or unwrap), all webhook delivery stops silently. Events accumulate in the channel forever with no recovery. Consider at minimum logging on panic via a wrapper, or adding a supervision restart mechanism.

6. Unnecessary LogEvent clone + repeated serialization per endpoint (CubeAPI/src/logging/http.rs:167, 181)

Each subscribed endpoint gets a deep-cloned HashMap. Plus, deliver_once serializes the event to JSON independently per endpoint even though the body is identical for endpoints without a secret. Consider serializing once and sharing the Vec<u8> body across endpoints.

7. env var mutation races with parallel tests (CubeAPI/src/config/mod.rs:449-453)

std::env::set_var mutates process-global state. Other tests running in parallel via cargo test (default) can see the poisoned value. Use a lock or refactor to inject env overrides as parameters.

8. Webhook receiver startup message doesn't match behavior (receiver.py:102)

Says "listening on .../webhook" but BaseHTTPRequestHandler dispatches all POST requests regardless of path — the handler never checks self.path. Either add path validation or correct the message.

🟡 Low Severity

  • No jitter in exponential backoff (http.rs:343-346) — retries synchronize across endpoints.
  • build_webhook_http_client panics on failure (http.rs:29-33) — could return Result.
  • cfg.webhooks.clone() at startup (main.rs:224) — could use std::mem::take.
  • Backoff formula undocumented (docs/guide/webhooks.md:53) — specify initial * 2^(attempt-1).
  • Empty events → all events not documented — code treats "events": [] as subscribe-all, which is useful but not documented.

✅ What's Done Well

  • Thorough SSRF protection in URL validation (scheme whitelist, IP blocklist, credentials rejection, redirect disabled).
  • HMAC secret correctly redacted in Debug output on both config and runtime types, verified by tests.
  • Timing-safe compare_digest in the Python receiver.
  • Clean separation of hot path (log() → channel send) from background delivery with retry.
  • Good test coverage for happy paths: delivery, HMAC signing, retries, concurrency isolation, redirect rejection, backoff arithmetic.
  • Strong documentation with a runnable receiver example and accurate Chinese translation.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds webhook delivery for CubeAPI sandbox lifecycle events by introducing an HTTP webhook logging backend, configuration via env/JSON, retry with exponential backoff, optional HMAC-SHA256 signing, and accompanying receiver/examples plus English/Chinese documentation.

Changes:

  • Implement HttpLogger webhook backend with async queueing, per-endpoint subscriptions, retries/backoff, and optional signatures.
  • Add CubeAPI configuration/env examples and docs pages for webhook payloads/headers/verification and a runnable stdlib receiver.
  • Wire webhook backend into CubeAPI startup logger fanout when endpoints are configured.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
examples/webhook-receiver/receiver.py Minimal Python stdlib webhook receiver with optional signature verification and WeCom forwarding
examples/webhook-receiver/README.md Run/config instructions and example payloads for the receiver
docs/guide/webhooks.md English integration guide for webhook configuration, payloads, headers, and signature verification
docs/zh/guide/webhooks.md Chinese integration guide mirroring the English webhook documentation
docs/.vitepress/config.mjs Adds webhook docs links to the Integration/集成 nav
deploy/one-click/env.example Documents webhook-related env vars for one-click deployments
CubeAPI/src/main.rs Enables webhook backend in the logger pipeline when configured
CubeAPI/src/logging/http.rs Implements HTTP webhook backend, endpoint filtering, delivery, retry/backoff, and signing + tests
CubeAPI/src/config/mod.rs Adds webhook endpoint and delivery tuning configuration via env vars
CubeAPI/README.md Documents webhook env vars and links to webhook guide
CubeAPI/Cargo.toml Adds crypto/encoding deps for HMAC-SHA256 signatures
CubeAPI/Cargo.lock Locks new Rust dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +251 to +253
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("webhook endpoint returned {status}: {text}");
Comment thread CubeAPI/src/config/mod.rs
Comment on lines +193 to +195
fn default_webhook_queue_capacity() -> usize {
env_parse("CUBE_API_WEBHOOK_QUEUE_CAPACITY", 1024)
}
Comment on lines +157 to +180
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
match msg {
Msg::Event(event) => {
let deliveries = endpoints
.iter()
.filter(|endpoint| endpoint_subscribes(endpoint, &event.event))
.cloned()
.map(|endpoint| {
deliver_with_retries(
client.clone(),
endpoint,
event.clone(),
options.clone(),
)
});
futures::future::join_all(deliveries).await;
}
Msg::Flush(reply) => {
let _ = reply.send(());
}
}
}
});
options: HttpWebhookOptions,
mut rx: mpsc::Receiver<Msg>,
) {
tokio::spawn(async move {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single worker is a throughput bottleneck. This spawn_worker dequeues events one at a time, and join_all waits for all endpoint deliveries to complete before picking up the next event. A single slow endpoint (or one that hits the 5s timeout) stalls delivery to all other endpoints and accelerates queue filling. Consider per-endpoint channels or spawning independent per-endpoint tasks so slow deliveries don't cascade.

Comment thread CubeAPI/src/logging/http.rs Outdated
}

let status = response.status();
let text = response.text().await.unwrap_or_default();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error body read failure. If response.text() fails (connection reset, encoding issue), its error is discarded and the logged error will say something like "webhook endpoint returned 500: " — losing diagnostic value. Log the read error before defaulting to empty string.

Comment thread CubeAPI/src/config/mod.rs
};
let events = std::env::var("CUBE_API_WEBHOOK_EVENTS")
.ok()
.map(|value| split_csv(&value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent fallback on parse failure. When an env var is present but unparseable (e.g., CUBE_API_WEBHOOK_QUEUE_CAPACITY=not-a-number), this returns the default with no warning. Operators won't know their config was ignored. Add a warn!() when the var exists but parsing fails.

Comment thread CubeAPI/src/logging/http.rs Outdated

type HmacSha256 = Hmac<Sha256>;

/// Configuration for the HTTP webhook backend.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code. HttpLoggerConfig and HttpLogger::new() are never used by the new webhook system — the only call site in main.rs uses from_endpoints(). The batch_size and flush_interval_secs fields are never read, but their names imply batching semantics that don't exist (events are forwarded one-at-a-time). Remove ~30 lines and the #[allow(dead_code)] annotation.

Comment thread examples/webhook-receiver/receiver.py Outdated


def main() -> None:
host = os.getenv("WEBHOOK_HOST", "0.0.0.0")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to 0.0.0.0 exposes the receiver to all network interfaces. This is the example receiver that users may deploy as-is. Combined with the optional nature of HMAC verification, this is a risk. Change the default to 127.0.0.1 and add a Content-Length cap (the current code reads however many bytes the header specifies).

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

@ceilf6/repo-guard CR

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

🛡️ ceilf6/repo-guard

代码评审报告: feat: issue-642

风险等级:
处理建议: 请求修改
决策摘要: 核心投递逻辑设计合理(背压丢弃、指数退避、HMAC 签名、每端点重试均已覆盖测试),但存在一处明确的安全/正确性问题(webhook secret 通过日志明文打印的风险 + Debug 派生泄漏)、一个可能导致事件丢失的静默失败路径,以及若干需要在合并前确认的配置/兼容性问题。

级联分析

  • 变更符号: HttpLoggerHttpLoggerConfigHttpWebhookEndpointHttpWebhookOptionsServerConfig(新增字段)、WebhookEndpointConfigasync_main
  • 受影响流程: 应用启动时的 logger 组装(async_main)、所有触发 LogEvent 的 sandbox 生命周期路径(create/delete/pause/resume)
  • 变更集外调用方: 本 PR 新增字段均带 #[serde(default = ...)],对现有 ServerConfig 消费者(如序列化到前端/配置面板的代码)需要确认是否有遗漏更新之处 (text search)
  • 置信度: medium — diff 内证据完整,但未见 GitNexus/代码图谱输出,ServerConfig 在项目内的其他消费点(如 admin API 序列化、配置校验)未在 diff 中确认是否需要同步调整

问题发现

  1. [高] HttpWebhookEndpoint 派生 Debug,secret 字段可能被意外打印到日志

    • 证据: CubeAPI/src/logging/http.rs:46-51 定义 pub struct HttpWebhookEndpoint { pub url, pub events, pub secret: Option<String> }#[derive(Debug, Clone, PartialEq, Eq)]config/mod.rsWebhookEndpointConfig 同样 #[derive(Debug, ...)] 且包含明文 secret
    • 受影响调用方/流程: 任何未来对这些结构体使用 {:?} 打印(例如调试日志、panic message、tracing::debug!(?endpoint, ...))都会把 HMAC 密钥写入日志文件或标准输出。
    • 最小可行修复: 为 secret 字段自定义 Debug(如 #[derive(Debug)] + 手写 impl fmt::Debug"***" 掩码),或引入 SecretString/redact 包装类型,防止未来误用泄漏。
  2. [中] webhook 投递失败时事件被静默丢弃,无死信/告警路径

    • 证据: CubeAPI/src/logging/http.rsdeliver_with_retries(约 150-190 行)在耗尽 max_attempts 后仅 error!(...) 打日志,事件即永久丢失;log()try_send 失败(队列满)也只 warn! 后丢弃(第 90-95 行附近)。
    • 受影响调用方/流程: 依赖 webhook 保证送达做审计或计费联动的下游系统会在网络抖动或短时下游故障期间静默丢事件,且没有指标暴露(如 webhook_delivery_failed_total)方便观测。
    • 最小可行修复: 至少补充一个可观测的失败计数器/metric,或在 README/webhooks 文档中明确声明“at-most-once, no persistence, no DLQ”的交付语义,避免调用方误以为是可靠投递。
  3. [中] CUBE_API_WEBHOOKS 解析失败时静默回退为空列表,且不校验重复/非法 URL

    • 证据: CubeAPI/src/config/mod.rs:152-163default_webhooks() 在 JSON 解析失败时只 tracing::warn! 然后继续走 CUBE_API_WEBHOOK_URLS 分支(即使用户本意是用 JSON 配置多端点),且未对 url 做协议/格式校验。
    • 受影响调用方/流程: 运维在 CUBE_API_WEBHOOKS 里写错 JSON(例如漏引号)时,服务会“悄悄”启动为无 webhook 或退化成单一简单配置,而不是启动失败,容易导致生产环境静默丢失通知能力却无人发现。
    • 最小可行修复: 至少在 default_webhooks() 解析失败时提升为 error! 级别并考虑 fail-fast(返回 Result 由调用方决定是否阻止启动),或在 README/webhooks.md 明确说明这种降级行为是预期设计。

行级发现

  • [CubeAPI/src/logging/http.rs:90] try_send 失败时事件被永久丢弃且无计数指标,仅有 warn! 日志,建议至少暴露 drop 计数供告警使用。
  • [CubeAPI/src/logging/http.rs:100] deliver_with_retries 在耗尽重试后只记录 error!,事件无持久化/死信队列,建议在文档中明确该语义或补充指标。
  • [CubeAPI/src/config/mod.rs:152] CUBE_API_WEBHOOKS JSON 解析失败仅 warn! 并静默回退到 CUBE_API_WEBHOOK_URLS 分支,建议提升日志级别或改为启动失败,避免生产环境配置错误被忽略。
  • [CubeAPI/src/main.rs:221] HttpLogger::from_endpoints 使用默认 reqwest::Client::new(),未复用应用级别可能已存在的连接池/超时配置(如有其他模块已持有共享 Client),可考虑复用以减少连接开销,非阻塞项。

Karpathy 评审

  • 假设: PR 假设 webhook 投递可以接受 at-most-once、无持久化的交付语义,这一假设未在文档中明确声明,消费者可能误以为是可靠投递。
  • 简洁性: 整体设计合理——Msg::Event/Msg::Flush channel 模式、按事件过滤订阅、指数退避均是恰当规模的实现,未见过度设计。HttpLogger::new 保留 legacy 单 URL 路径是必要的向后兼容,未引入不必要抽象。
  • 结构质量: spawn_worker 中对同一事件的多端点投递使用 futures::future::join_all,一个慢端点会拖慢整批事件的下一次 dequeue(worker 是单一 mpsc receiver 循环,未对每个事件做真正并发流水线);在端点数量增多或某端点长期不可用时,可能造成事件处理延迟累积直至队列打满。建议评估是否需要为高延迟场景做背压说明或限流。
  • 变更范围: PR 范围合理聚焦在 webhook 功能实现、配置、文档和示例,未见无关重构或范围蔓延。
  • 验证: 单测覆盖了 JSON 投递、HMAC 签名、重试逻辑、订阅匹配四个关键路径,覆盖较全面;但未见对“重试耗尽后事件丢弃”“JSON 解析失败回退”等边缘路径的测试,也未见集成测试验证 async_main 中 webhook logger 与 MultiLogger/FilteredLogger 的组装正确性。

缺失覆盖

  • default_webhooks()CUBE_API_WEBHOOKS JSON 格式错误时的降级行为(当前测试未覆盖,需明确预期行为并补测试)。
  • 队列满(try_send 返回 Err)时事件丢弃路径的行为测试或至少 warn 日志的可观测性验证。
  • max_attempts 耗尽后不重复投递、且不影响后续事件处理的集成级验证(当前 logger_retries_failed_delivery 只验证了最终成功场景,未验证永久失败场景)。
  • webhook secret 是否会出现在任何 {:?} 输出路径的静态检查/测试(建议补充一个断言 format!("{:?}", endpoint) 不包含明文 secret 的测试用例)。

Autonomously-by: Codex:GPT-5
}
}
}
result = deliveries.join_next(), if !deliveries.is_empty() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JoinSet starvation under sustained load — The biased tokio::select! prioritizes rx.recv() (line 154). When events arrive continuously, the deliveries.join_next() arm at line 184 is never polled because rx.recv() is always immediately ready. Completed delivery tasks accumulate in the JoinSet without being joined, causing unbounded memory growth over time.

Fix: Drain completed tasks non-blockingly after processing each event using JoinSet::try_join_next() before re-entering select!:

while let Ok(Some(result)) = deliveries.try_join_next() {
    if let Err(err) = result {
        warn!(error = %err, "HttpLogger: webhook delivery task failed");
    }
}

Comment thread CubeAPI/src/config/mod.rs
Ok(url.to_string())
}

fn validate_webhook_ip_literal(ip: IpAddr, raw_url: &str) -> anyhow::Result<()> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSRF bypass via IPv4-mapped IPv6 addressesvalidate_webhook_ip_literal delegates to separate branches for IPv4 and IPv6 without checking IPv4-mapped IPv6 addresses (::ffff:x.x.x.x). An attacker can encode a private IP (e.g., 10.0.0.1, 169.254.169.254) as [::ffff:a00:1] or [::ffff:a9fe:a9fe]. The IPv6 validator (is_allowed_webhook_ipv6) sees these as non-loopback, non-local, non-multicast addresses and returns allowed = true, bypassing SSRF protection entirely.

Fix: Check for embedded IPv4 before delegating to the IPv6 validator:

IpAddr::V6(ip) => {
    if let Some(ipv4) = ip.to_ipv4_mapped() {
        is_allowed_webhook_ipv4(ipv4)
    } else {
        is_allowed_webhook_ipv6(ip)
    }
}

Comment thread CubeAPI/src/main.rs Outdated
let endpoints: Vec<HttpWebhookEndpoint> =
cfg.webhooks.clone().into_iter().map(Into::into).collect();
let webhook_logger = HttpLogger::from_endpoints(
reqwest::Client::new(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrestricted HTTP redirect policyreqwest::Client::new() defaults to following up to 10 cross-origin redirects. A compromised or malicious webhook endpoint could return a 302/307 redirect pointing at an internal service (e.g., cloud metadata API, internal Redis), and the CubeAPI server would follow it without validation.

Fix: Build the client with a restrictive redirect policy:

reqwest::Client::builder()
    .redirect(reqwest::redirect::Policy::none())
    .build()
    .expect("failed to build webhook HTTP client")

Comment thread CubeAPI/src/config/mod.rs
let host = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("webhook URL must include a host: {raw_url}"))?;
if let Ok(ip) = host.parse::<IpAddr>() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DNS hostname SSRF — no IP resolution validation — When the webhook URL uses a DNS name rather than an IP literal, the code skips IP validation entirely (the if let Ok(ip) = host.parse::<IpAddr>() guard at line 295). A DNS name pointing at metadata.google.internal or 10.0.0.1.internal passes validation despite resolving to a private IP.

Fix: Resolve DNS names at config time and validate all resolved addresses against the same IP blocklist. Note this requires making validate_webhook_url async (or using synchronous DNS).

Comment thread CubeAPI/src/config/mod.rs
tracing::warn!(url = %endpoint.url, "ignoring duplicate webhook endpoint URL");
return None;
}
endpoint.events = endpoint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty event list silently subscribes to ALL events — When the event filter cleanup removes all entries (e.g., user passes "events": [""]), the resulting empty Vec causes endpoint_subscribes (http.rs:202-208) to return true for every event. This silently converts an invalid config into an all-events subscription. A user with a typo or misplaced comma would unknowingly receive events they didn't intend to subscribe to.

Fix: Log a warning when cleaning removes all explicitly-provided events, so this behavior is at least observable.

options: HttpWebhookOptions,
mut rx: mpsc::Receiver<Msg>,
) {
tokio::spawn(async move {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worker JoinHandle not stored — no graceful shutdown path — The spawn_worker JoinHandle at line 149 is discarded. During shutdown, the axum server stops, flush() drains pending deliveries, and then HttpLogger is dropped — but the worker task is cancelled by the runtime before its drain_deliveries on channel-close can complete. Storing the handle and awaiting it on shutdown would ensure all pending deliveries finish before the process exits.

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

@ceilf6/repo-guard CR again

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

🛡️ ceilf6/repo-guard

代码评审报告: feat: issue-642

风险等级:
处理建议: 评论
决策摘要: 核心投递机制(重试、背压、HMAC、SSRF 防护)设计合理且有测试覆盖;存在几个值得在合并前处理的正确性/健壮性问题(TOCTOU DNS rebinding、无界并发 spawn、try_send 丢事件无指标),但都不是阻塞级别,建议评论后可合并。

级联分析

  • 变更符号: HttpLogger::from_endpoints(新公共构造函数,替换旧 stub HttpLogger::new)、ServerConfig(新增 5 个 webhook 字段)、WebhookEndpointConfig(新类型)
  • 受影响流程: async_main 启动流程新增 webhook logger 分支;沙箱生命周期事件(create/delete/pause/resume)通过既有 LogEvent 管道被下游 HttpLogger 消费
  • 变更集外调用方: 搜索未发现其他文件引用旧 HttpLogger::new/HttpLoggerConfig(已被完全替换,属破坏性但因原实现是 stub 且从未接线,无实际调用方受影响)(text search)
  • 置信度: medium — diff 内部自洽,测试覆盖投递/重试/签名/背压路径,但未用代码图谱验证 LogEvent 全部产生点(sandbox create/delete/pause/resume handler)是否都设置了 event.event 字段与文档承诺一致

问题发现

  1. [中] SSRF 校验存在 DNS rebinding / TOCTOU 窗口

    • 证据: validate_webhook_url 只在配置加载时校验一次 URL 的 host 是否为 IP literal 并检查是否为私网地址(CubeAPI/src/config/mod.rs:257-296)。若 url 使用域名而非 IP literal,该域名可以在启动后解析到内网地址,或首次解析为公网 IP 通过校验后,后续被攻击者更改 DNS 记录指向内网/云元数据地址(169.254.169.254 等)。reqwest::Client 在每次实际请求时会重新解析 DNS,绕过启动期校验。
    • 受影响调用方/流程: 所有通过域名配置的 webhook 端点在服务运行期间存在 SSRF 风险,尤其是 cloud metadata endpoint (169.254.169.254 属于 link-local,正常会被 IPv4 分支拒绝,但只在 URL 直接写 IP 时生效,写域名则完全绕过)。
    • 最小可行修复: 在文档中明确声明当前仅对 IP-literal URL 做启动期校验,域名不做运行时反 DNS-rebinding 防护;或在 deliver_once 请求前对已解析 IP 做二次校验(需要自定义 resolver/connector),后者工作量较大,建议至少更新文档并在 issue 中跟踪。
  2. [中] 单个事件的并发 fan-out 到多端点使用无界 JoinSet::spawn,缺少全局并发上限

    • 证据: spawn_worker 中每收到一个 Msg::Event,会对所有匹配端点各自 deliveries.spawn(...)CubeAPI/src/logging/http.rs:145-160)。虽然每个 endpoint 有 Semaphore::new(DEFAULT_ENDPOINT_CONCURRENCY)(值为 4)限流,但 deliveries: JoinSet<()> 本身没有总量上限,当端点数量多或事件突发时,正在等待 semaphore permit 的 task 数量可以无限增长,每个都持有 LogEvent clone + HttpWebhookEndpoint clone,可能造成内存增长。
    • 受影响调用方/流程: 高吞吐场景(大量沙箱并发创建/删除)叠加慢端点时的 worker 内存占用。
    • 最小可行修复: 在 spawn 前先 acquire semaphore permit(当前是在 spawned task 内部才 acquire,导致 task 本身已经创建并持有 clone 数据),或者给 JoinSet 增加基于配置的最大在途任务数保护。
  3. [低] log() 方法用 try_send 静默丢弃事件,没有可观测的丢弃计数

    • 证据: CubeAPI/src/logging/http.rs:96-101,队列满时只打一条 warn! 日志,没有 metric/counter。
    • 受影响调用方/流程: 生产环境下游 webhook 消费者若长时间下线,队列积压后事件会被静默丢弃,运维只能通过翻日志发现,难以监控告警。
    • 最小可行修复: 非阻塞,可后续加 metric;不阻塞本次合并。

行级发现

  • [CubeAPI/src/logging/http.rs:56] DEFAULT_ENDPOINT_CONCURRENCY = 4 硬编码不可配置,与 PR 暴露的其他调优参数(timeout/attempts/backoff)风格不一致,建议至少加入 HttpWebhookOptions 或注释说明为何固定为 4。
  • [CubeAPI/src/logging/http.rs:145] 事件 fan-out 循环内为每个匹配端点 spawn task 后才在 task 内部 acquire_owned,导致背压发生在 task 已创建之后,未能真正限制排队中的 task 数量,见发现 2。
  • [CubeAPI/src/logging/http.rs:250] anyhow::bail! 错误信息中包含被截断到 2048 字节的响应体,若端点返回敏感信息(如反射了请求 token)会被写入 CubeAPI 自身日志;建议评估是否需要对响应体做进一步脱敏或限制记录级别。
  • [CubeAPI/src/config/mod.rs:270] validate_webhook_url 只在 host 为 IP literal 时做私网校验,域名 host 完全跳过该检查,见发现 1,建议在文档 docs/guide/webhooks.md 明确这一限制范围。
  • [CubeAPI/src/config/mod.rs:288] http + secret 组合仅记录 warning 放行,中间人可窃取签名密钥对应的明文 payload 与签名值从而重放;生产环境建议提供强制拒绝的开关(例如 CUBE_API_WEBHOOK_REQUIRE_HTTPS_WITH_SECRET),当前实现无法在配置层面强制这一点。
  • [CubeAPI/src/main.rs:228] endpoints: Vec<HttpWebhookEndpoint> = cfg.webhooks.clone().into_iter().map(Into::into).collect()cfg 做了一次 clone,此调用只在启动时执行一次,性能上无影响,可忽略。

Karpathy 评审

  • 假设: PR 假设 webhook 消费者能容忍 at-most-once 语义且不需要死信队列,文档中已明确声明这一限制,属于合理的最小实现假设。未发现关联 issue,无法核对是否有更严格的可靠性验收标准。
  • 简洁性: 整体实现克制,没有过度抽象;HttpWebhookEndpoint/HttpWebhookOptions/EndpointWorker 划分清晰,未见猜测性通用化。
  • 结构质量: logging/http.rs 从 stub 变为 607 行完整实现,仍在合理边界内,未跨越可扫描性阈值;SSRF 校验逻辑放在 config/mod.rs 而非 logging/http.rs,边界划分合理(配置校验属于加载期职责)。
  • 变更范围: diff 集中在 webhook 功能本身及必要的文档/配置样例,无明显无关重构或格式噪声。
  • 验证: 测试覆盖 JSON 投递、HMAC 签名、重试、慢端点不阻塞快端点、私网 IP 拒绝、重复端点去重、debug 脱敏,覆盖面较好;缺少域名 host 校验行为的测试(例如确认非 IP host 不会被误拒绝),以及 webhook 队列满时丢弃行为的测试。

缺失覆盖

  • 队列满(try_send 失败)路径的行为测试,确认事件丢弃不会影响后续事件正常处理。
  • 域名(非 IP literal)webhook URL 的校验行为测试,明确当前实现对域名不做私网解析检查这一已知限制。
  • max_attempts 全部失败后是否有任何可观测信号(当前只有 error! 日志,无测试验证该路径行为,虽已有 logger_retries_failed_delivery 覆盖单次重试成功场景)。

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

@ceilf6/repo-guard CR again

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

🛡️ ceilf6/repo-guard

代码评审报告: feat: issue-642

风险等级:
处理建议: 请求修改
决策摘要: 核心投递路径设计良好(SSRF 防护、redirect 拒绝、per-endpoint 并发、HMAC 签名),但存在一个 TOCTOU 级别的 DNS rebinding 缺口,且配置校验中的 fail-open 行为在系统层面有安全含义,需要先处理再合并。

级联分析

  • 变更符号: HttpLogger::from_endpoints(新公共构造函数,替代 stub HttpLogger::new)、ServerConfig(新增 5 个字段)、WebhookEndpointConfig(新类型)、validate_webhook_url/clean_webhook_endpoints(新校验路径)。
  • 受影响流程: sandbox 生命周期事件(create/delete/pause/resume)的日志分发路径,async_main 中 logger 初始化。
  • 变更集外调用方: 未发现旧 HttpLogger::new/HttpLoggerConfig 的外部调用方(原实现是 stub,#[allow(dead_code)]),本次是纯新增能力,无破坏性签名变更。MultiLogger/FilteredLogger/Logger trait 契约未变。
  • 置信度: medium(基于 diff 与文本搜索;未使用代码图谱验证 HttpLoggerConfig/HttpLogger::new 在其他 crate 或脚本中完全无引用)。

问题发现

  1. [高] DNS rebinding: 启动时校验的地址与实际请求时解析的地址可能不同

    • 证据: validate_webhook_hostname 在配置加载阶段(default_webhooks()clean_webhook_endpoints)用 (host, port).to_socket_addrs() 解析主机名并校验 IP 是否在允许范围内,但实际 HTTP 请求由 reqwest::Clientdeliver_once 中发出,会重新解析 DNS。攻击者控制的域名可以在校验通过后(例如启动时解析到 1.2.3.4)改变 DNS 记录,使运行期请求解析到 169.254.169.254 或内网地址,SSRF 防护被绕过。
    • 受影响调用方/流程: 所有使用主机名(而非 IP literal)配置的 webhook endpoint,尤其是长期运行的 CubeAPI 进程。
    • 最小可行修复: 在 deliver_once 发起请求前,对目标 URL 的解析结果做同样的 IP 校验(例如自定义 reqwest::dns::Resolve 实现,或在发送前手动 resolve + 校验 + 通过 IP 直连并设置 Host header),而不是只在配置加载时一次性校验。文档中提到"DNS check is a startup-time guard"承认了这个限制,但对于默认启用的安全特性,建议至少在 README/webhooks.md 中更明确地标注这是已知限制,或者实现请求时校验作为默认防护。
  2. [中] 配置解析失败时静默 fail-open,可能导致 webhook 静默失效或部分生效

    • 证据: CUBE_API_WEBHOOKS JSON 解析失败时仅 eprintln! + tracing::error!,返回空列表(不是校验失败中止启动);单个 endpoint URL 校验失败、重复 URL 也是静默跳过。这在配置错误时会导致运维认为 webhook 已配置生效,实际上部分或全部 endpoint 未加载,且没有任何强信号(如启动失败)暴露给部署方。
    • 受影响调用方/流程: 依赖 webhook 通知做审计、监控或自动化响应的下游系统。
    • 最小可行修复: 考虑区分"格式错误应阻止启动"与"单个 endpoint 无效应跳过"两类错误;至少确保这些警告级别日志在生产环境默认可见(当前 tracing::warn/error 依赖 min_level 配置,若 LOG_LEVEL 设置过高可能被过滤掉)。
  3. [中] default_webhooks() 在测试中修改全局环境变量,存在测试间竞态风险

    • 证据: env_parse_clamps_queue_capacity_to_effective_minimum 测试用 std::env::set_var/remove_var 操作全局状态,Rust 测试默认并行执行,若同一 crate 内其他测试并发读取相关环境变量(如 default_webhooks() 依赖的 CUBE_API_WEBHOOK_URLS 等),存在数据竞争导致 flaky test 的风险。
    • 受影响调用方/流程: CI 测试稳定性。
    • 最小可行修复: 使用 serial_test crate 或等价机制串行化涉及环境变量的测试,或改为依赖注入而非直接读环境变量。

行级发现

  • [CubeAPI/src/config/mod.rs:224] default_webhooks() 中 JSON 解析失败仅记录日志后返回空 Vec::new()(对应函数末尾的 fallback 路径),属于 fail-open;见 Finding 2,建议在文档或行为上明确这是有意设计还是应收紧。
  • [CubeAPI/src/config/mod.rs:329] validate_webhook_hostname 依赖 to_socket_addrs() 做同步 DNS 解析,该调用是阻塞的,default_webhooks()ServerConfig::default()(同步上下文)中被调用;虽然只在启动时执行一次可接受,但如果未来 DNS 解析变慢,会拖慢进程启动。可接受,仅供记录。
  • [CubeAPI/src/config/mod.rs:301] validate_webhook_urlhttp + secret 组合只告警不阻止(scheme 检查在 293-296 行),与 Finding 1 的 rebinding 风险叠加时,HTTP 明文 + 可被 DNS rebinding 绕过的 SSRF 防护构成组合风险;建议至少在生产模式下考虑拒绝而非警告。
  • [CubeAPI/src/logging/http.rs:349] signature_header 使用 HmacSha256::new_from_slice 对任意长度密钥没有强制最小长度校验,弱密钥(如空字符串会被上层 filter(|s| !s.is_empty()) 挡掉,但极短密钥不受限)会削弱 HMAC 防护强度;非阻塞,可选加固。
  • [CubeAPI/src/logging/http.rs:171] try_send 在队列满或 receiver 已关闭时静默丢弃事件并只记录 warn;这是有意为之的背压策略(避免阻塞主流程),设计合理,但建议在 docs/guide/webhooks.md 中已经提到"best-effort",与实现一致,无需改动。

Karpathy 评审

  • 假设: 实现假设"启动时 DNS 校验 + IP 白名单"足以防御 SSRF;这在静态 IP 或短生命周期部署下成立,但对长期运行进程+动态域名场景不成立(rebinding)。文档中已诚实标注这一限制(webhooks.md 提到"DNS check is a startup-time guard"),说明作者已意识到该假设的边界,值得肯定,但作为默认安全特性建议进一步收紧或至少提升可见性。
  • 简洁性: 整体实现克制,没有过度抽象。EndpointWorker + Semaphore per-endpoint 并发控制、JoinSet 管理投递任务的设计合理,没有引入不必要的通用层。
  • 结构质量: deliver_with_retries/deliver_once/response_body_summary 职责清晰,clean_webhook_endpoints 把校验、去重、trim 逻辑集中在一处,符合 canonical ownership。未发现明显的结构退化或重复造轮子。
  • 变更范围: 变更集中在新增能力(webhook 投递 + 配置 + 文档 + 示例),未见无关重构或格式噪声。logging/mod.rs 的 doc 表格更新准确反映了状态变化。
  • 验证: 测试覆盖较全面——正常投递、HMAC 签名、重试、redirect 拒绝、per-endpoint 并发隔离(slow endpoint 不阻塞 fast endpoint)、配置校验(私网 IP、IPv4-mapped IPv6、重复 URL、无效 scheme)均有对应测试。缺口:没有针对 DNS rebinding 场景的测试(即启动校验通过后 DNS 记录变化的场景),也没有测试 webhook 配置解析失败时的进程行为(是否应该阻止启动)。

缺失覆盖

  • 请求时(而非仅启动时)SSRF/rebinding 防护的测试或至少已知限制的显式验收。
  • 私网 hostname(非 IP literal,如内部服务发现域名解析到私网地址)在 validate_webhook_hostname 路径下的负向测试用例(当前测试集中在 IP literal 校验,hostname 路径只有一个 loopback 正向用例)。
  • 集成测试验证 async_main 中 webhook logger 只在 cfg.webhooks 非空时才被加入 MultiLogger(当前只有单元测试覆盖 HttpLogger 本身,未覆盖 wiring 逻辑)。

@fslongjin

Copy link
Copy Markdown
Member

Refs #642

@fslongjin

Copy link
Copy Markdown
Member

Hello, thank you for your contribution! Since we have a large number of participants in this event, to speed up the PR review process, could you please attach some screenshots and logs (both are needed) showing the verification process and results for this PR? Thank you for your participation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants