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:
@@ -750,7 +750,31 @@ impl IntoResponse for ApiError {
|
|||||||
ExecError::OperationBudgetExceeded => {
|
ExecError::OperationBudgetExceeded => {
|
||||||
(StatusCode::INSUFFICIENT_STORAGE, e.to_string())
|
(StatusCode::INSUFFICIENT_STORAGE, e.to_string())
|
||||||
}
|
}
|
||||||
ExecError::Runtime(_) => (StatusCode::BAD_GATEWAY, e.to_string()),
|
ExecError::Runtime(detail) => {
|
||||||
|
// Audit 2026-06-11 (F-SE-H-04) — runtime / SDK error
|
||||||
|
// strings leak internal detail to the public data
|
||||||
|
// plane: filesystem paths, "blocked by SSRF policy:
|
||||||
|
// link-local" (confirms cloud-metadata reachability),
|
||||||
|
// pool-exhaustion timing, and panic/backtrace
|
||||||
|
// fragments. They also reflect attacker-thrown
|
||||||
|
// strings (`throw "..."`) back into operator-facing
|
||||||
|
// JSON. This handler only ever serves anonymous
|
||||||
|
// data-plane callers (execute-by-id + user routes;
|
||||||
|
// the authenticated script-test path lives in
|
||||||
|
// manager-core and keeps verbose errors), so log the
|
||||||
|
// full text server-side under a correlation id and
|
||||||
|
// return only a stable generic message + the id.
|
||||||
|
let correlation_id = Uuid::new_v4();
|
||||||
|
tracing::warn!(
|
||||||
|
%correlation_id,
|
||||||
|
detail = %detail,
|
||||||
|
"script runtime error (detail scrubbed from response)"
|
||||||
|
);
|
||||||
|
(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
format!("script execution error (ref: {correlation_id})"),
|
||||||
|
)
|
||||||
|
}
|
||||||
ExecError::Overloaded { .. } => unreachable!("handled above"),
|
ExecError::Overloaded { .. } => unreachable!("handled above"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1234,6 +1234,45 @@ async fn execution_errors_are_still_logged(pool: PgPool) {
|
|||||||
assert!(logs[0]["response_body"]["error"].is_string());
|
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
|
// v1.1.3 — Modules: scripts.kind, route + trigger rejection, end-to-end import
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user