fix(audit-2026-06-11/F-SE-H-04): scrub runtime error detail from public data-plane responses

ExecError::Runtime strings (filesystem paths, "blocked by SSRF policy:
link-local" cloud-metadata reconnaissance, pool-exhaustion timing,
panic fragments, and attacker-thrown strings) were returned verbatim in
the public /api/v1/execute + user-route 502 bodies. This handler only
serves anonymous data-plane callers (the authenticated script-test path
is in manager-core and keeps verbose errors), so log the full text
server-side under a correlation id and return a stable generic message
plus the id for operator grep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-12 18:23:58 +02:00
parent 95fddf31bc
commit e676eb9ba7
2 changed files with 64 additions and 1 deletions

View File

@@ -1234,6 +1234,45 @@ async fn execution_errors_are_still_logged(pool: PgPool) {
assert!(logs[0]["response_body"]["error"].is_string());
}
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[sqlx::test(migrations = "../manager-core/migrations")]
async fn runtime_error_detail_is_scrubbed_from_public_response(pool: PgPool) {
// Audit 2026-06-11 (F-SE-H-04) — a runtime error's verbose detail
// (here a thrown marker string, but in the wild SDK/SSRF/internal
// paths) must not reach the public data-plane response body. The
// caller gets a stable message + a correlation id; the full text
// only goes to the server log.
let (s, app_id) = server_with_app(pool).await;
let created: Value = s
.post("/api/v1/admin/scripts")
.json(&with_app(
&app_id,
json!({
"name": "leaky",
"source": r#"throw "SECRET_MARKER_98765""#,
}),
))
.await
.json();
let id = created["id"].as_str().unwrap();
let r = s
.post(&format!("/api/v1/execute/{id}"))
.json(&json!({}))
.await;
r.assert_status(axum::http::StatusCode::BAD_GATEWAY);
let body: Value = r.json();
let err = body["error"].as_str().unwrap();
assert!(
!err.contains("SECRET_MARKER_98765"),
"runtime detail leaked to public response: {err}"
);
assert!(
err.contains("ref:"),
"scrubbed response should carry a correlation id, got: {err}"
);
}
// ============================================================================
// v1.1.3 — Modules: scripts.kind, route + trigger rejection, end-to-end import
// ============================================================================