mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix(wasm): run leak scan before credential injection in tools wrapper (#791)
* fix(wasm): run leak scan before credential injection in tools wrapper The tools WASM wrapper runs the LeakDetector on HTTP request headers AFTER inject_host_credentials() has already substituted real secrets (e.g., xoxb- Slack bot tokens). This causes the leak detector to flag the tool's own legitimate outbound API calls as secret exfiltration. Move the scan to run on raw_headers before any credential injection, matching the fix already applied to the channels wrapper in #421. Fixes the same class of bug as #421 (which only fixed channels/wasm/wrapper.rs). Co-Authored-By: Claude Opus 4.6 <[email protected]> * perf: inline leak scan to avoid Vec allocation on every HTTP request Address review feedback: instead of cloning all header keys/values into a Vec to pass to scan_http_request(), iterate over raw_headers directly using scan_and_clean(). This also provides more specific error messages (URL vs header vs body). Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix cargo fmt formatting in leak scan loop Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c148dd2b5b
commit
66e834d9d7
+74
-10
@@ -279,6 +279,27 @@ impl near::agent::host::Host for StoreData {
|
||||
let raw_headers: HashMap<String, String> =
|
||||
serde_json::from_str(&headers_json).unwrap_or_default();
|
||||
|
||||
// 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.
|
||||
// Inline the scan to avoid allocating a Vec of cloned headers.
|
||||
let leak_detector = LeakDetector::new();
|
||||
leak_detector
|
||||
.scan_and_clean(&injected_url)
|
||||
.map_err(|e| format!("Potential secret leak in URL blocked: {}", e))?;
|
||||
for (name, value) in &raw_headers {
|
||||
leak_detector.scan_and_clean(value).map_err(|e| {
|
||||
format!("Potential secret leak in header '{}' blocked: {}", name, e)
|
||||
})?;
|
||||
}
|
||||
if let Some(body_bytes) = body.as_deref() {
|
||||
let body_str = String::from_utf8_lossy(body_bytes);
|
||||
leak_detector
|
||||
.scan_and_clean(&body_str)
|
||||
.map_err(|e| format!("Potential secret leak in body blocked: {}", e))?;
|
||||
}
|
||||
|
||||
let mut headers: HashMap<String, String> = raw_headers
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
@@ -297,16 +318,6 @@ impl near::agent::host::Host for StoreData {
|
||||
self.inject_host_credentials(&host, &mut headers, &mut url);
|
||||
}
|
||||
|
||||
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(&url, &header_vec, body.as_deref())
|
||||
.map_err(|e| format!("Potential secret leak blocked: {}", e))?;
|
||||
|
||||
// Get the max response size from capabilities (default 10MB).
|
||||
let max_response_bytes = self
|
||||
.host_state
|
||||
@@ -1792,4 +1803,57 @@ mod tests {
|
||||
// Should remain as string since it can't be parsed
|
||||
assert_eq!(result["count"], serde_json::json!("not-a-number"));
|
||||
}
|
||||
|
||||
/// Regression test: leak scan must run on raw headers (before credential
|
||||
/// injection), not after. If it ran post-injection, the host-injected
|
||||
/// Slack bot token (`xoxb-...`) would trigger a Block and reject the
|
||||
/// tool's own legitimate outbound request.
|
||||
#[test]
|
||||
fn test_leak_scan_runs_before_credential_injection() {
|
||||
use crate::safety::LeakDetector;
|
||||
|
||||
// Simulate pre-injection headers: WASM only sees the placeholder, not the real token.
|
||||
let raw_headers: Vec<(String, String)> = vec![
|
||||
(
|
||||
"Authorization".to_string(),
|
||||
"Bearer {SLACK_BOT_TOKEN}".to_string(),
|
||||
),
|
||||
("Content-Type".to_string(), "application/json".to_string()),
|
||||
];
|
||||
|
||||
let detector = LeakDetector::new();
|
||||
|
||||
// Pre-injection scan should pass — placeholders are not secrets.
|
||||
let pre_result = detector.scan_http_request(
|
||||
"https://slack.com/api/chat.postMessage",
|
||||
&raw_headers,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
pre_result.is_ok(),
|
||||
"Leak scan on pre-injection headers should pass, but got: {:?}",
|
||||
pre_result
|
||||
);
|
||||
|
||||
// Post-injection headers would contain a real Slack token.
|
||||
let post_injection_headers: Vec<(String, String)> = vec![
|
||||
(
|
||||
"Authorization".to_string(),
|
||||
"Bearer xoxb-1234567890-abcdefghij".to_string(),
|
||||
),
|
||||
("Content-Type".to_string(), "application/json".to_string()),
|
||||
];
|
||||
|
||||
// Post-injection scan WOULD block — this is the false positive
|
||||
// that the pre-injection ordering prevents.
|
||||
let post_result = detector.scan_http_request(
|
||||
"https://slack.com/api/chat.postMessage",
|
||||
&post_injection_headers,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
post_result.is_err(),
|
||||
"Leak scan on post-injection headers should block the Slack token"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user