/// Path-safety policy for templated resource parameters,
/// mirroring fastmcp/resources/security.py.
///|
/// Whether `value` contains `..` as a path component, considering both
/// separators. Dots inside a single segment are not traversal.
pub fn contains_path_traversal(value : String) -> Bool {
for segment in value.split("/") {
for piece in segment.to_owned().split("\\") {
if piece.to_owned() == ".." {
return true
}
}
}
false
}
///|
/// Whether `value` looks like an absolute filesystem path: rooted at `/`
/// or `\\`, UNC-prefixed, or a Windows drive letter path.
pub fn is_absolute_path(value : String) -> Bool {
if value.has_prefix("/") || value.has_prefix("\\\\") {
return true
}
if value.has_prefix("\\") {
return true
}
// Drive letter form: `C:\` or `C:/`.
let chars = value.to_array()
if chars.length() >= 3 {
let drive = chars[0]
let is_letter = (drive >= 'a' && drive <= 'z') ||
(drive >= 'A' && drive <= 'Z')
if is_letter && chars[1] == ':' && (chars[2] == '/' || chars[2] == '\\') {
return true
}
}
false
}
///|
/// Security policy applied to extracted resource template parameters.
///
/// The checks run after a URI has matched a template and its parameter
/// values have been extracted and percent-decoded; they catch
/// path-traversal and absolute-path injection regardless of how the value
/// was encoded in the URI.
pub(all) struct ResourceSecurity {
/// Reject values containing `..` as a path component.
reject_path_traversal : Bool
/// Reject values that look like absolute filesystem paths.
reject_absolute_paths : Bool
/// Reject values containing NUL.
reject_null_bytes : Bool
/// Parameter names to skip all checks for. Hyphenated URI-template
/// spellings are accepted.
exempt_params : @set.Set[String]
} derive(Debug, Eq)
///|
/// Secure-by-default policy: traversal, absolute paths and null bytes
/// all rejected.
pub fn ResourceSecurity::default() -> ResourceSecurity {
{
reject_path_traversal: true,
reject_absolute_paths: true,
reject_null_bytes: true,
exempt_params: @set.Set::from_array([]),
}
}
///|
fn ResourceSecurity::is_exempt(self : ResourceSecurity, name : String) -> Bool {
if self.exempt_params.contains(name) {
return true
}
let mut found = false
for exempt in self.exempt_params.iter() {
if exempt.replace(old="-", new="_") == name {
found = true
break
}
}
found
}
///|
/// Check all parameter values against the policy. String values (and lists
/// of strings, from wildcard parameters) are screened; non-string values are
/// ignored. Returns the name of the first failing parameter, if any.
pub fn ResourceSecurity::validate(
self : ResourceSecurity,
params : Map[String, Json],
) -> String? {
for name, value in params {
if self.is_exempt(name) {
continue
}
let candidates : Array[String] = match value {
String(text) => [text]
Array(items) =>
items.filter_map(fn(item) {
if item is String(s) {
Some(s)
} else {
None
}
})
_ => []
}
for candidate in candidates {
if self.reject_null_bytes && candidate.contains("\u{0}") {
return Some(name)
}
if self.reject_path_traversal && contains_path_traversal(candidate) {
return Some(name)
}
if self.reject_absolute_paths && is_absolute_path(candidate) {
return Some(name)
}
}
}
None
}
///|
/// Per-component security setting: inherit the server-wide default,
/// apply an explicit policy, or disable screening.
pub(all) enum ResourceSecuritySetting {
Inherit
Explicit(ResourceSecurity)
Disabled
} derive(Debug, Eq)
///|
/// Resolve the effective policy for a component.
pub fn resolve_security(
setting : ResourceSecuritySetting,
server_default : ResourceSecurity?,
) -> ResourceSecurity? {
match setting {
Inherit => server_default
Explicit(policy) => Some(policy)
Disabled => None
}
}
///|
pub extend ResourceSecurity with Debug::{to_repr}
///|
pub extend ResourceSecurity with Eq::{not_equal, equal}
///|
pub extend ResourceSecuritySetting with Debug::{to_repr}
///|
pub extend ResourceSecuritySetting with Eq::{not_equal, equal}