mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-02 23:56:24 +08:00
fix(wasm): run leak scan on pre-injection headers in channel callbacks (#1377)
* fix(wasm): run leak scan on pre-injection headers in channel callbacks
The WASM channel host's http_request handler was scanning request headers
AFTER inject_credentials() replaced placeholder values (e.g. {SLACK_BOT_TOKEN})
with real secrets. This caused the leak detector to flag host-injected
credentials as potential leaks, blocking legitimate WASM channel callbacks.
Run the leak scan on the original WASM-provided headers (before any
credential injection) so host-injected tokens never appear in the scan.
WASM never sees the real values, so scanning the pre-injection state is
correct. Matches the existing pattern in src/tools/wasm/wrapper.rs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: regression test for pre-injection leak scan ordering
Proves that scanning post-injection headers triggers a false positive
on host-injected xoxb- tokens, confirming the fix must scan WASM-provided
headers before credential injection.
* fix: address review feedback — eliminate double-parse, fix comment, migrate import
- Eliminate double-parse of headers_json: parse once, scan raw headers,
then inject credentials (matches tools wrapper pattern)
- Fix misleading comment: URL has template substitution but not yet
host credential injection (was "before ANY credential injection")
- Migrate import to ironclaw_safety::LeakDetector per CLAUDE.md
- Remove unnecessary block scope around leak scan
- Remove raw_url_for_scan alias (just use &url directly)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
This commit is contained in:
@@ -339,11 +339,31 @@ impl near::agent::channel_host::Host for ChannelStoreData {
|
||||
format!("Rate limit exceeded: {}", e)
|
||||
})?;
|
||||
|
||||
// Parse headers and inject credentials into header values
|
||||
// This allows patterns like "Authorization": "Bearer {WHATSAPP_ACCESS_TOKEN}"
|
||||
let raw_headers: std::collections::HashMap<String, String> =
|
||||
serde_json::from_str(&headers_json).unwrap_or_default();
|
||||
// Parse headers from WASM and scan for leaks before credential injection.
|
||||
// Host-injected tokens (e.g., xoxb- Slack bot token) would otherwise
|
||||
// trigger the leak detector. The URL has template substitution applied
|
||||
// (`injected_url`) but not yet host credential injection.
|
||||
let raw_headers: std::collections::HashMap<String, String> = serde_json::from_str(
|
||||
&headers_json,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(error = %e, "Malformed headers JSON from WASM; scanning empty headers");
|
||||
std::collections::HashMap::new()
|
||||
});
|
||||
|
||||
let mut logical_url = injected_url;
|
||||
|
||||
let leak_detector = LeakDetector::new();
|
||||
let raw_header_vec: Vec<(String, String)> = raw_headers
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
leak_detector
|
||||
.scan_http_request(&logical_url, &raw_header_vec, body.as_deref())
|
||||
.map_err(|e| format!("Potential secret leak blocked: {}", e))?;
|
||||
|
||||
// Now inject credentials into header values
|
||||
// This allows patterns like "Authorization": "Bearer {WHATSAPP_ACCESS_TOKEN}"
|
||||
let mut headers: std::collections::HashMap<String, String> = raw_headers
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
@@ -363,22 +383,6 @@ impl near::agent::channel_host::Host for ChannelStoreData {
|
||||
"Parsed and injected request headers"
|
||||
);
|
||||
|
||||
let mut logical_url = injected_url;
|
||||
|
||||
// Leak scan runs on WASM-provided values BEFORE host credential injection.
|
||||
// This prevents false positives where the host-injected Bearer token
|
||||
// (e.g., xoxb- Slack token) triggers the leak detector — WASM never saw
|
||||
// the real value, so scanning the pre-injection state is correct.
|
||||
let leak_detector = LeakDetector::new();
|
||||
let header_vec: Vec<(String, String)> = headers
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
leak_detector
|
||||
.scan_http_request(&logical_url, &header_vec, body.as_deref())
|
||||
.map_err(|e| format!("Potential secret leak blocked: {}", e))?;
|
||||
|
||||
// Inject pre-resolved host credentials (Bearer tokens, API keys, etc.)
|
||||
// after the leak scan so host-injected secrets don't trigger false positives.
|
||||
if let Some(host) = extract_host_from_url(&logical_url) {
|
||||
@@ -6834,18 +6838,87 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewrite_http_url_for_testing_uses_host_map() {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
let _lock = ENV_MUTEX
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env mutex poisoned");
|
||||
fn test_http_request_scans_headers_before_placeholder_substitution() {
|
||||
use super::ChannelStoreData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let _guard = crate::config::helpers::lock_env();
|
||||
let original = std::env::var(TEST_HTTP_REWRITE_MAP_ENV).ok();
|
||||
|
||||
// SAFETY: guarded by ENV_MUTEX — no concurrent env access.
|
||||
// SAFETY: Under ENV_MUTEX, no concurrent env access.
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
TEST_HTTP_REWRITE_MAP_ENV,
|
||||
r#"{"slack.com":"http://127.0.0.1:1"}"#,
|
||||
);
|
||||
}
|
||||
|
||||
let capabilities =
|
||||
ChannelCapabilities::for_channel("test").with_tool_capabilities(ToolCapabilities {
|
||||
http: Some(HttpCapability::new(vec![EndpointPattern::host(
|
||||
"slack.com",
|
||||
)])),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Placeholder whose substituted value matches the openai_api_key leak
|
||||
// pattern (`sk-(?:proj-)?[a-zA-Z0-9]{20,}`). If the scan ran AFTER
|
||||
// `inject_credentials` replaced `{FAKE_TOKEN}` in the Authorization
|
||||
// header, the Bearer value would trip the detector.
|
||||
let mut credentials = HashMap::new();
|
||||
credentials.insert(
|
||||
"FAKE_TOKEN".to_string(),
|
||||
"sk-proj-TESTFAKEKEY01234567890abcdef".to_string(),
|
||||
);
|
||||
|
||||
let mut store = ChannelStoreData::new(
|
||||
1024 * 1024,
|
||||
"test",
|
||||
capabilities,
|
||||
credentials,
|
||||
Vec::new(),
|
||||
Arc::new(PairingStore::new_noop()),
|
||||
);
|
||||
|
||||
let result = super::near::agent::channel_host::Host::http_request(
|
||||
&mut store,
|
||||
"BREW".to_string(),
|
||||
"https://slack.com/api/chat.postMessage".to_string(),
|
||||
r#"{"Authorization":"Bearer {FAKE_TOKEN}"}"#.to_string(),
|
||||
None,
|
||||
Some(1_000),
|
||||
);
|
||||
|
||||
// Reaching the unsupported-method branch proves the leak scan accepted
|
||||
// the raw `Bearer {FAKE_TOKEN}` header value. A regression to
|
||||
// post-injection scanning would surface as "Potential secret leak
|
||||
// blocked" before method dispatch.
|
||||
let error = result.expect_err("unsupported method should fail after leak scan");
|
||||
assert!(
|
||||
!error.contains("Potential secret leak blocked"),
|
||||
"placeholder-substituted credential must not trigger request leak scan: {error}"
|
||||
);
|
||||
assert!(
|
||||
error.contains("Unsupported HTTP method: BREW"),
|
||||
"expected unsupported method after leak scan, got: {error}"
|
||||
);
|
||||
|
||||
// SAFETY: Under ENV_MUTEX, restore original state.
|
||||
unsafe {
|
||||
if let Some(ref val) = original {
|
||||
std::env::set_var(TEST_HTTP_REWRITE_MAP_ENV, val);
|
||||
} else {
|
||||
std::env::remove_var(TEST_HTTP_REWRITE_MAP_ENV);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewrite_http_url_for_testing_uses_host_map() {
|
||||
let _guard = crate::config::helpers::lock_env();
|
||||
let original = std::env::var(TEST_HTTP_REWRITE_MAP_ENV).ok();
|
||||
|
||||
// SAFETY: Under ENV_MUTEX, no concurrent env access.
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
TEST_HTTP_REWRITE_MAP_ENV,
|
||||
|
||||
Reference in New Issue
Block a user