[model-gateway] fix WASM arbitrary file read security vol (#14664)
This commit is contained in:
@@ -10,11 +10,15 @@
|
|||||||
//! 5. ValidateWasmComponent - Validate WASM component format
|
//! 5. ValidateWasmComponent - Validate WASM component format
|
||||||
//! 6. RegisterModule - Register module in WasmModuleManager
|
//! 6. RegisterModule - Register module in WasmModuleManager
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{
|
||||||
|
path::{Component as PathComponent, Path},
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use wasmtime::{component::Component, Config, Engine};
|
use wasmtime::{component::Component, Config, Engine};
|
||||||
|
|
||||||
@@ -31,6 +35,34 @@ pub struct WasmModuleConfigRequest {
|
|||||||
pub descriptor: WasmModuleDescriptor,
|
pub descriptor: WasmModuleDescriptor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sensitive system directories that WASM modules cannot be loaded from.
|
||||||
|
/// These are blocked to prevent information disclosure attacks.
|
||||||
|
const BLOCKED_PATH_PREFIXES: &[&str] = &[
|
||||||
|
"/etc/",
|
||||||
|
"/proc/",
|
||||||
|
"/sys/",
|
||||||
|
"/dev/",
|
||||||
|
"/boot/",
|
||||||
|
"/root/",
|
||||||
|
"/var/log/",
|
||||||
|
"/var/run/",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Check if a path starts with any blocked prefix.
|
||||||
|
/// Returns the matched prefix if found, None otherwise.
|
||||||
|
fn find_blocked_prefix(path: &str) -> Option<&'static str> {
|
||||||
|
BLOCKED_PATH_PREFIXES
|
||||||
|
.iter()
|
||||||
|
.find(|&&prefix| path.starts_with(prefix))
|
||||||
|
.copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a path has a .wasm extension (case-insensitive).
|
||||||
|
fn has_wasm_extension(path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("wasm"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Step 1: Validate module descriptor
|
/// Step 1: Validate module descriptor
|
||||||
///
|
///
|
||||||
/// Validates that the module descriptor has all required fields:
|
/// Validates that the module descriptor has all required fields:
|
||||||
@@ -67,6 +99,64 @@ impl StepExecutor for ValidateDescriptorStep {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Security: Validate path to prevent path traversal attacks
|
||||||
|
let path = Path::new(&descriptor.file_path);
|
||||||
|
|
||||||
|
// Must be an absolute path
|
||||||
|
if !path.is_absolute() {
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: format!(
|
||||||
|
"Module file path must be absolute, got: {}",
|
||||||
|
descriptor.file_path
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal components (.. or symbolic links that could escape)
|
||||||
|
for component in path.components() {
|
||||||
|
match component {
|
||||||
|
PathComponent::ParentDir => {
|
||||||
|
warn!(
|
||||||
|
"Path traversal attempt detected in WASM module path: {}",
|
||||||
|
descriptor.file_path
|
||||||
|
);
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: "Path traversal (..) not allowed in module file path".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PathComponent::CurDir => {
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: "Current directory (.) not allowed in module file path"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require .wasm extension to prevent loading arbitrary files
|
||||||
|
if !has_wasm_extension(path) {
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: "Module file must have .wasm extension".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block access to sensitive system directories
|
||||||
|
if let Some(prefix) = find_blocked_prefix(&descriptor.file_path) {
|
||||||
|
warn!(
|
||||||
|
"Attempt to access blocked directory in WASM module path: {}",
|
||||||
|
descriptor.file_path
|
||||||
|
);
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: format!("Access to {} directory is not allowed", prefix),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if file exists and get size
|
// Check if file exists and get size
|
||||||
let metadata = tokio::fs::metadata(&descriptor.file_path)
|
let metadata = tokio::fs::metadata(&descriptor.file_path)
|
||||||
.await
|
.await
|
||||||
@@ -82,6 +172,45 @@ impl StepExecutor for ValidateDescriptorStep {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canonicalize the path to resolve symlinks and verify final location is safe
|
||||||
|
let canonical_path = tokio::fs::canonicalize(&descriptor.file_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: format!(
|
||||||
|
"Failed to canonicalize path {}: {}",
|
||||||
|
descriptor.file_path, e
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Re-check blocked directories after symlink resolution
|
||||||
|
let canonical_str = canonical_path.to_string_lossy();
|
||||||
|
if let Some(prefix) = find_blocked_prefix(&canonical_str) {
|
||||||
|
warn!(
|
||||||
|
"Symlink resolved to blocked directory: {} -> {}",
|
||||||
|
descriptor.file_path, canonical_str
|
||||||
|
);
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: format!(
|
||||||
|
"Path resolves to blocked directory {} (via symlink)",
|
||||||
|
prefix
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure canonicalized path still has .wasm extension (symlink target check)
|
||||||
|
if !has_wasm_extension(&canonical_path) {
|
||||||
|
warn!(
|
||||||
|
"Symlink target is not a .wasm file: {} -> {}",
|
||||||
|
descriptor.file_path, canonical_str
|
||||||
|
);
|
||||||
|
return Err(WorkflowError::StepFailed {
|
||||||
|
step_id: StepId::new("validate_descriptor"),
|
||||||
|
message: "Symlink target must be a .wasm file".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Store file size in context for later steps
|
// Store file size in context for later steps
|
||||||
context.set("file_size_bytes", metadata.len());
|
context.set("file_size_bytes", metadata.len());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user