Skip to content

Commit d63601e

Browse files
serrrfiratclaude
andauthored
fix(security): gate test URL rewriters behind #[cfg(test)] (fixes #2056) (#2401)
The `rewrite_telegram_api_url_for_testing()`, `rewrite_http_url_for_testing()`, and their supporting constants/helpers were gated behind `#[cfg(any(test, debug_assertions))]`, which means they shipped in all debug builds — including development/staging deployments. An attacker who could set `IRONCLAW_TEST_TELEGRAM_API_BASE_URL` or `IRONCLAW_TEST_HTTP_REWRITE_MAP` environment variables on such a deployment could redirect Telegram API traffic (and other HTTP traffic) to an arbitrary host. Changes: - Narrow all test URL rewrite constants, functions, and helpers from `#[cfg(any(test, debug_assertions))]` to `#[cfg(test)]` - Add missing `#[cfg(test)]` to `TELEGRAM_TEST_API_BASE_ENV` (was ungated) - Wrap the call site in `http_request()` with `#[cfg(test)]`/`#[cfg(not(test))]` blocks so production builds use `logical_url` directly - Remove the now-unnecessary `#[cfg(not(...))]` stub functions that returned None Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 28c6a15 commit d63601e

1 file changed

Lines changed: 26 additions & 25 deletions

File tree

src/channels/wasm/wrapper.rs

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ use crate::tools::wasm::{
6262
};
6363
use ironclaw_safety::LeakDetector;
6464

65-
#[cfg(any(test, debug_assertions))]
65+
#[cfg(test)]
6666
const TEST_HTTP_REWRITE_MAP_ENV: &str = "IRONCLAW_TEST_HTTP_REWRITE_MAP";
6767

6868
const WEBSOCKET_EVENT_QUEUE_RELATIVE_PATH: &str = "state/gateway_event_queue";
6969
const WEBSOCKET_EVENT_PROCESSING_QUEUE_RELATIVE_PATH: &str = "state/gateway_event_queue_processing";
7070
const WEBSOCKET_EVENT_QUEUE_MAX_ITEMS: usize = 100;
71+
#[cfg(test)]
7172
const TELEGRAM_TEST_API_BASE_ENV: &str = "IRONCLAW_TEST_TELEGRAM_API_BASE_URL";
7273

7374
// Generate component model bindings from the WIT file
@@ -384,17 +385,27 @@ impl near::agent::channel_host::Host for ChannelStoreData {
384385
self.inject_host_credentials(&host, &mut headers, &mut logical_url);
385386
}
386387

387-
let rewritten_transport_url = rewrite_http_url_for_testing(&logical_url)
388-
.or_else(|| rewrite_telegram_api_url_for_testing(&logical_url));
389-
let allow_private_test_target = rewritten_transport_url.is_some();
390-
let transport_url = rewritten_transport_url.unwrap_or_else(|| logical_url.clone());
391-
if transport_url != logical_url {
392-
tracing::info!(
393-
logical_url = %logical_url,
394-
transport_url = %transport_url,
395-
"Rewriting outbound HTTP request to test base URL"
396-
);
397-
}
388+
let (transport_url, allow_private_test_target) = {
389+
#[cfg(test)]
390+
{
391+
let rewritten = rewrite_http_url_for_testing(&logical_url)
392+
.or_else(|| rewrite_telegram_api_url_for_testing(&logical_url));
393+
let is_rewritten = rewritten.is_some();
394+
let url = rewritten.unwrap_or_else(|| logical_url.clone());
395+
if url != logical_url {
396+
tracing::info!(
397+
logical_url = %logical_url,
398+
transport_url = %url,
399+
"Rewriting outbound HTTP request to test base URL"
400+
);
401+
}
402+
(url, is_rewritten)
403+
}
404+
#[cfg(not(test))]
405+
{
406+
(logical_url.clone(), false)
407+
}
408+
};
398409

399410
// Get the max response size from capabilities (default 10MB).
400411
let max_response_bytes = self
@@ -4118,7 +4129,7 @@ fn extract_host_from_url(url: &str) -> Option<String> {
41184129
///
41194130
/// The replacement preserves the original path and query string so tests can
41204131
/// point production hosts at local fakes without adding channel-specific code.
4121-
#[cfg(any(test, debug_assertions))]
4132+
#[cfg(test)]
41224133
fn rewrite_http_url_for_testing(url: &str) -> Option<String> {
41234134
let parsed = url::Url::parse(url).ok()?;
41244135
if !matches!(parsed.scheme(), "http" | "https") {
@@ -4139,12 +4150,7 @@ fn rewrite_http_url_for_testing(url: &str) -> Option<String> {
41394150
Some(rewritten)
41404151
}
41414152

4142-
#[cfg(not(any(test, debug_assertions)))]
4143-
fn rewrite_http_url_for_testing(_url: &str) -> Option<String> {
4144-
None
4145-
}
4146-
4147-
#[cfg(any(test, debug_assertions))]
4153+
#[cfg(test)]
41484154
fn parse_test_http_rewrite_map(raw: &str) -> HashMap<String, String> {
41494155
let trimmed = raw.trim();
41504156
if trimmed.is_empty() {
@@ -4174,7 +4180,7 @@ fn parse_test_http_rewrite_map(raw: &str) -> HashMap<String, String> {
41744180
}
41754181
}
41764182

4177-
#[cfg(any(test, debug_assertions))]
4183+
#[cfg(test)]
41784184
fn rewrite_telegram_api_url_for_testing(url: &str) -> Option<String> {
41794185
let override_base = std::env::var(TELEGRAM_TEST_API_BASE_ENV)
41804186
.ok()
@@ -4199,11 +4205,6 @@ fn rewrite_telegram_api_url_for_testing(url: &str) -> Option<String> {
41994205
}
42004206
Some(rewritten)
42014207
}
4202-
4203-
#[cfg(not(any(test, debug_assertions)))]
4204-
fn rewrite_telegram_api_url_for_testing(_url: &str) -> Option<String> {
4205-
None
4206-
}
42074208
fn should_skip_response_leak_scan(url: &str) -> bool {
42084209
url::Url::parse(url).is_ok_and(|parsed| {
42094210
matches!(parsed.scheme(), "http" | "https")

0 commit comments

Comments
 (0)