use crate::{
metadata::{
customattributes::{CustomAttributeArgument, CustomAttributeValue},
typesystem::CilType,
validation::{
context::{OwnedValidationContext, ValidationContext},
traits::OwnedValidator,
},
},
Error, Result,
};
use rustc_hash::FxHashSet;
pub struct OwnedSecurityValidator;
impl OwnedSecurityValidator {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl OwnedSecurityValidator {
fn validate_security_permission_declarations(
&self,
context: &OwnedValidationContext,
) -> Result<()> {
let security_declarations = context.object().security_declarations();
for entry in security_declarations {
let (_token, decl_security) = (entry.key(), entry.value());
if !Self::is_valid_security_action(decl_security.action.into()) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Security declaration has invalid action: {:?}",
decl_security.action
),
});
}
let permission_set = &decl_security.permission_set;
if let crate::metadata::security::PermissionSetFormat::Xml = permission_set.format() {
let xml_content = String::from_utf8_lossy(permission_set.raw_data());
self.validate_permission_set_format(&xml_content)?;
if let Some(conflict) = Self::detect_permission_conflicts(&xml_content) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Security declaration has permission conflict: {conflict}"
),
});
}
} else {
let raw_data = permission_set.raw_data();
if raw_data.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Security declaration has empty permission set data".to_string(),
});
}
}
}
Ok(())
}
fn validate_permission_set_format(&self, permission_set: &str) -> Result<()> {
if permission_set.is_empty() {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Empty permission set in security declaration".to_string(),
});
}
if !permission_set.trim_start().starts_with('<')
|| !permission_set.trim_end().ends_with('>')
{
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Permission set is not valid XML".to_string(),
});
}
if !permission_set.contains("PermissionSet") {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Permission set missing PermissionSet element".to_string(),
});
}
if permission_set.len() > 100_000 {
let set_len = permission_set.len();
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!("Permission set is excessively large ({set_len} characters)"),
});
}
if Self::has_suspicious_permission_patterns(permission_set) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: "Permission set contains suspicious patterns".to_string(),
});
}
Ok(())
}
fn is_valid_security_action(action: u16) -> bool {
matches!(action, 1..=14)
}
fn detect_permission_conflicts(permission_set: &str) -> Option<String> {
if permission_set.contains("Deny") && permission_set.contains("Assert") {
let deny_perms = Self::extract_permission_types(permission_set, "Deny");
let assert_perms = Self::extract_permission_types(permission_set, "Assert");
for deny_perm in &deny_perms {
if assert_perms.contains(deny_perm) {
return Some(format!(
"Conflict: Deny and Assert on same permission: {deny_perm}"
));
}
}
}
if permission_set.contains("PermitOnly") {
let permit_perms = Self::extract_permission_types(permission_set, "PermitOnly");
if permit_perms.len() > 1 {
return Some("Multiple PermitOnly declarations conflict".to_string());
}
}
None
}
fn extract_permission_types(permission_set: &str, action: &str) -> Vec<String> {
let mut permissions = Vec::new();
if let Some(start) = permission_set.find(&format!("<{action}")) {
if let Some(end) = permission_set[start..].find('>') {
let section = &permission_set[start..start + end];
if let Some(class_start) = section.find("class=\"") {
if let Some(class_end) = section[class_start + 7..].find('"') {
let class_name = §ion[class_start + 7..class_start + 7 + class_end];
permissions.push(class_name.to_string());
}
}
}
}
permissions
}
fn has_suspicious_permission_patterns(permission_set: &str) -> bool {
let dangerous_patterns = [
"UnmanagedCode",
"SkipVerification",
"ControlEvidence",
"ControlPolicy",
"SerializationFormatter",
"ControlPrincipal",
"ControlThread",
"Infrastructure",
"FullTrust",
];
for pattern in &dangerous_patterns {
if permission_set.contains(pattern) {
let count = permission_set.matches(pattern).count();
if count > 3 {
return true;
}
}
}
if permission_set.contains("<script")
|| permission_set.contains("javascript:")
|| permission_set.contains("vbscript:")
{
return true;
}
let nesting_level = permission_set.matches('<').count();
if nesting_level > 100 {
return true;
}
false
}
fn validate_code_access_security_attributes(
&self,
context: &OwnedValidationContext,
) -> Result<()> {
let methods = context.object().methods();
for type_entry in context.all_types() {
for (_, custom_attr) in type_entry.custom_attributes.iter() {
if Self::is_security_attribute(custom_attr) {
self.validate_security_attribute_usage(custom_attr, "Type", &type_entry.name)?;
}
}
}
for method_entry in methods {
let method = method_entry.value();
for (_, custom_attr) in method.custom_attributes.iter() {
if Self::is_security_attribute(custom_attr) {
self.validate_security_attribute_usage(custom_attr, "Method", &method.name)?;
}
}
}
Ok(())
}
fn is_security_attribute(custom_attr: &CustomAttributeValue) -> bool {
custom_attr.fixed_args.iter().any(|arg| {
if let CustomAttributeArgument::String(s) = arg {
s.contains("Security") || s.contains("Permission") || s.contains("Principal")
} else {
false
}
})
}
fn validate_security_attribute_usage(
&self,
custom_attr: &CustomAttributeValue,
target_type: &str,
target_name: &str,
) -> Result<()> {
if custom_attr.fixed_args.len() > 10 {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Security attribute on {} '{}' has excessive arguments ({})",
target_type,
target_name,
custom_attr.fixed_args.len()
),
});
}
for arg in &custom_attr.fixed_args {
if let CustomAttributeArgument::String(s) = arg {
if Self::has_dangerous_security_content(s) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Security attribute on {target_type} '{target_name}' contains dangerous content"
),
});
}
}
}
Ok(())
}
fn has_dangerous_security_content(content: &str) -> bool {
let dangerous_patterns = [
"cmd.exe",
"powershell",
"regedit",
"format c:",
"rm -rf",
"del /s",
"<script",
"javascript:",
"vbscript:",
"file://",
"\\\\",
];
dangerous_patterns
.iter()
.any(|pattern| content.to_lowercase().contains(pattern))
}
fn validate_security_transparency(&self, context: &OwnedValidationContext) -> Result<()> {
let mut critical_types = FxHashSet::default();
let mut transparent_types = FxHashSet::default();
let all_types = context.all_types();
for type_entry in all_types {
let is_critical = Self::has_security_critical_attribute(type_entry);
let is_transparent = Self::has_security_transparent_attribute(type_entry);
if is_critical && is_transparent {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Type '{}' cannot be both SecurityCritical and SecurityTransparent",
type_entry.name
),
});
}
if is_critical {
critical_types.insert(type_entry.token.value());
}
if is_transparent {
transparent_types.insert(type_entry.token.value());
}
}
for type_entry in all_types {
if let Some(base_type) = type_entry.base() {
let type_token = type_entry.token.value();
let base_token = base_type.token.value();
if transparent_types.contains(&type_token) && critical_types.contains(&base_token) {
return Err(Error::ValidationOwnedFailed {
validator: self.name().to_string(),
message: format!(
"Transparent type '{}' cannot inherit from critical base type",
type_entry.name
),
});
}
}
}
Ok(())
}
fn has_security_critical_attribute(type_entry: &CilType) -> bool {
type_entry.custom_attributes.iter().any(|(_, attr)| {
attr.fixed_args.iter().any(|arg| {
if let CustomAttributeArgument::String(s) = arg {
s.contains("SecurityCritical")
} else {
false
}
})
})
}
fn has_security_transparent_attribute(type_entry: &CilType) -> bool {
type_entry.custom_attributes.iter().any(|(_, attr)| {
attr.fixed_args.iter().any(|arg| {
if let CustomAttributeArgument::String(s) = arg {
s.contains("SecurityTransparent")
} else {
false
}
})
})
}
}
impl OwnedValidator for OwnedSecurityValidator {
fn validate_owned(&self, context: &OwnedValidationContext) -> Result<()> {
self.validate_security_permission_declarations(context)?;
self.validate_code_access_security_attributes(context)?;
self.validate_security_transparency(context)?;
Ok(())
}
fn name(&self) -> &'static str {
"OwnedSecurityValidator"
}
fn priority(&self) -> u32 {
120
}
fn should_run(&self, context: &OwnedValidationContext) -> bool {
context.config().enable_semantic_validation
}
}
impl Default for OwnedSecurityValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[cfg_attr(feature = "skip-expensive-tests", allow(unused_imports))]
mod tests {
use super::*;
use crate::{
metadata::validation::ValidationConfig,
test::{
factories::validation::system_security::owned_security_validator_file_factory,
owned_validator_test,
},
};
#[test]
#[cfg(not(feature = "skip-expensive-tests"))]
fn test_owned_security_validator() -> Result<()> {
let validator = OwnedSecurityValidator::new();
let config = ValidationConfig {
enable_semantic_validation: true,
..Default::default()
};
owned_validator_test(
owned_security_validator_file_factory,
"OwnedSecurityValidator",
"ValidationOwnedFailed",
config,
|context| validator.validate_owned(context),
)
}
}