#![cfg_attr(coverage_nightly, coverage(off))]
use super::types::Severity;
use anyhow::Result;
#[derive(Debug, Clone)]
pub struct SecurityValidation {
pub passed: bool,
pub issues: Vec<SecurityIssue>,
}
#[derive(Debug, Clone)]
pub struct SecurityIssue {
pub severity: Severity,
pub description: String,
pub category: SecurityCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityCategory {
InvalidFormat,
MemorySafety,
ResourceExhaustion,
CodeInjection,
Other,
}
pub struct WasmSecurityValidator;
impl WasmSecurityValidator {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Self {
Self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn validate(&self, data: &[u8]) -> Result<SecurityValidation> {
let mut issues = Vec::new();
if data.len() < 8 {
issues.push(SecurityIssue {
severity: Severity::Critical,
description: "File too small to be valid WASM".to_string(),
category: SecurityCategory::InvalidFormat,
});
} else if &data[0..4] != b"\0asm" {
issues.push(SecurityIssue {
severity: Severity::Critical,
description: "Invalid WASM magic number".to_string(),
category: SecurityCategory::InvalidFormat,
});
}
if data.len() > 100 * 1024 * 1024 {
issues.push(SecurityIssue {
severity: Severity::High,
description: "File size exceeds safe limit (100MB)".to_string(),
category: SecurityCategory::ResourceExhaustion,
});
}
Ok(SecurityValidation {
passed: issues.is_empty(),
issues,
})
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn validate_text(&self, content: &str) -> Result<SecurityValidation> {
let mut issues = Vec::new();
for (index, line) in content.lines().enumerate() {
let line_no = index + 1;
Self::check_memory_declaration(line, line_no, &mut issues);
Self::check_unsafe_constructs(line, line_no, &mut issues);
}
Ok(SecurityValidation {
passed: issues.is_empty(),
issues,
})
}
fn check_memory_declaration(line: &str, line_no: usize, issues: &mut Vec<SecurityIssue>) {
let Some(start) = line.find("(memory") else {
return;
};
let rest = &line[start + "(memory".len()..];
let decl = rest.split(')').next().unwrap_or(rest);
let limits: Vec<u64> = decl
.split_whitespace()
.filter_map(|token| {
token
.trim_matches(|c: char| !c.is_ascii_digit())
.parse()
.ok()
})
.collect();
let Some(&min) = limits.first() else {
return;
};
if limits.len() == 1 {
issues.push(SecurityIssue {
severity: Severity::Medium,
description: format!(
"line {line_no}: linear memory declared with initial {min} page(s) and no \
maximum — the module can grow memory without bound"
),
category: SecurityCategory::ResourceExhaustion,
});
}
if min > MAX_SAFE_INITIAL_PAGES {
issues.push(SecurityIssue {
severity: Severity::High,
description: format!(
"line {line_no}: linear memory reserves {min} pages \
({} MiB) before the module runs",
min / 16
),
category: SecurityCategory::ResourceExhaustion,
});
}
}
fn check_unsafe_constructs(line: &str, line_no: usize, issues: &mut Vec<SecurityIssue>) {
const MEMORY_SAFETY: &[(&str, &str)] = &[
(
"changetype<",
"changetype<> reinterprets a value as another type without a check",
),
(
"load<",
"load<T>() reads linear memory directly, bypassing bounds checks",
),
(
"store<",
"store<T>() writes linear memory directly, bypassing bounds checks",
),
];
for (needle, why) in MEMORY_SAFETY {
if line.contains(needle) {
issues.push(SecurityIssue {
severity: Severity::Medium,
description: format!("line {line_no}: {why}"),
category: SecurityCategory::MemorySafety,
});
}
}
if line.contains("memory.grow") {
issues.push(SecurityIssue {
severity: Severity::Low,
description: format!("line {line_no}: memory.grow grows linear memory at runtime"),
category: SecurityCategory::ResourceExhaustion,
});
}
}
}
pub const MAX_SAFE_INITIAL_PAGES: u64 = 1024;
impl Default for WasmSecurityValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_validator_new() {
let validator = WasmSecurityValidator::new();
let _ = validator;
}
#[test]
fn test_security_validator_default() {
let validator = WasmSecurityValidator;
let _ = validator;
}
#[test]
fn test_validate_valid_wasm_header() {
let validator = WasmSecurityValidator::new();
let data = b"\0asm\x01\x00\x00\x00";
let result = validator.validate(data).unwrap();
assert!(result.passed);
assert!(result.issues.is_empty());
}
#[test]
fn test_validate_too_small() {
let validator = WasmSecurityValidator::new();
let data = b"\0asm"; let result = validator.validate(data).unwrap();
assert!(!result.passed);
assert_eq!(result.issues.len(), 1);
assert!(matches!(
result.issues[0].category,
SecurityCategory::InvalidFormat
));
}
#[test]
fn test_validate_invalid_magic() {
let validator = WasmSecurityValidator::new();
let data = b"invalid\x00"; let result = validator.validate(data).unwrap();
assert!(!result.passed);
assert_eq!(result.issues.len(), 1);
assert!(result.issues[0].description.contains("magic number"));
}
#[test]
fn test_validate_text_clean_module() {
let validator = WasmSecurityValidator::new();
let result = validator.validate_text("(module)").unwrap();
assert!(result.passed);
assert!(result.issues.is_empty());
}
#[test]
fn test_validate_text_flags_unbounded_memory() {
let validator = WasmSecurityValidator::new();
let result = validator.validate_text("(module\n (memory 1))").unwrap();
assert!(!result.passed, "unbounded memory must be reported");
assert_eq!(result.issues.len(), 1);
assert_eq!(
result.issues[0].category,
SecurityCategory::ResourceExhaustion
);
assert!(
result.issues[0].description.contains("line 2"),
"issues name the line they were found on: {}",
result.issues[0].description
);
}
#[test]
fn test_validate_text_bounded_memory_is_clean() {
let validator = WasmSecurityValidator::new();
let result = validator.validate_text("(module (memory 1 4))").unwrap();
assert!(
result.passed,
"a maximum bounds the growth: {:?}",
result.issues
);
}
#[test]
fn test_validate_text_flags_oversized_initial_memory() {
let validator = WasmSecurityValidator::new();
let result = validator
.validate_text("(module (memory 4096 8192))")
.unwrap();
assert!(!result.passed);
assert!(result
.issues
.iter()
.any(|issue| issue.severity == Severity::High
&& issue.category == SecurityCategory::ResourceExhaustion));
}
#[test]
fn test_validate_text_flags_assemblyscript_escape_hatches() {
let validator = WasmSecurityValidator::new();
let source = "export function f(p: usize): i32 {\n return load<i32>(p);\n}\n";
let result = validator.validate_text(source).unwrap();
assert!(!result.passed, "raw load<T>() must be reported");
assert_eq!(result.issues[0].category, SecurityCategory::MemorySafety);
}
#[test]
fn test_security_category_eq() {
assert_eq!(
SecurityCategory::InvalidFormat,
SecurityCategory::InvalidFormat
);
assert_ne!(
SecurityCategory::InvalidFormat,
SecurityCategory::MemorySafety
);
}
#[test]
fn test_security_category_debug() {
let category = SecurityCategory::MemorySafety;
let debug_str = format!("{:?}", category);
assert!(debug_str.contains("MemorySafety"));
}
#[test]
fn test_security_issue_clone() {
let issue = SecurityIssue {
severity: Severity::High,
description: "test issue".to_string(),
category: SecurityCategory::Other,
};
let cloned = issue.clone();
assert_eq!(issue.description, cloned.description);
}
#[test]
fn test_security_validation_clone() {
let validation = SecurityValidation {
passed: true,
issues: vec![],
};
let cloned = validation.clone();
assert_eq!(validation.passed, cloned.passed);
}
#[test]
fn test_all_security_categories() {
let _ = SecurityCategory::InvalidFormat;
let _ = SecurityCategory::MemorySafety;
let _ = SecurityCategory::ResourceExhaustion;
let _ = SecurityCategory::CodeInjection;
let _ = SecurityCategory::Other;
}
}