///|
/// Raised when a path escapes the jail in non-recovering mode.
pub suberror SecurityError {
SecurityError(String)
} derive(Debug)
///|
/// Resolves system and web paths (Ruby `PathResolver`). POSIX separators only.
pub struct PathResolver {
mut working_dir : String
priv sys_cache : Map[String, (Array[String], String?)]
priv web_cache : Map[String, (Array[String], String?)]
}
///|
pub fn PathResolver::new(working_dir? : String = "/") -> PathResolver {
{ working_dir, sys_cache: {}, web_cache: {}, }
}
///|
pub fn PathResolver::is_absolute(_self : PathResolver, path : String) -> Bool {
path.has_prefix("/")
}
///|
pub fn PathResolver::is_root(self : PathResolver, path : String) -> Bool {
self.is_absolute(path)
}
///|
pub fn PathResolver::is_unc(_self : PathResolver, path : String) -> Bool {
path.has_prefix("//")
}
///|
pub fn PathResolver::is_web_root(_self : PathResolver, path : String) -> Bool {
path.has_prefix("/")
}
///|
/// Offset at which `path` descends from `base`, or None.
pub fn PathResolver::descends_from(
_self : PathResolver,
path : String,
base : String,
) -> Int? {
if base == path {
Some(0)
} else if base == "/" {
if path.has_prefix("/") {
Some(1)
} else {
None
}
} else if path.has_prefix(base + "/") {
Some(base.length() + 1)
} else {
None
}
}
///|
/// Path of `path` relative to `base` (Ruby `relative_path`).
pub fn PathResolver::relative_path(
self : PathResolver,
path : String,
base : String,
) -> String {
if !self.is_root(path) {
return path
}
match self.descends_from(path, base) {
Some(offset) => @rb.from(path, offset)
None => {
// Pathname#relative_path_from
let (ps, _) = self.partition_path(self.expand_path(path))
let (bs, _) = self.partition_path(self.expand_path(base))
let mut i = 0
while i < ps.length() && i < bs.length() && ps[i] == bs[i] {
i += 1
}
let parts = []
for _ in i.. String {
path.unwrap_or("")
}
///|
/// Resolves `..` segments (Ruby `expand_path`, no filesystem access).
pub fn PathResolver::expand_path(self : PathResolver, path : String) -> String {
let (segments, root) = self.partition_path(path)
if path.contains("..") {
let resolved = []
for seg in segments {
if seg == ".." {
resolved.pop() |> ignore
} else {
resolved.push(seg)
}
}
join_path(resolved, root)
} else {
join_path(segments, root)
}
}
///|
/// Splits a path into segments and root.
pub fn PathResolver::partition_path(
self : PathResolver,
path : String,
web? : Bool = false,
) -> (Array[String], String?) {
let cache = if web { self.web_cache } else { self.sys_cache }
match cache.get(path) {
Some(r) => return (r.0.copy(), r.1)
None => ()
}
let posix_path = path
let root : String? = if web {
if posix_path.has_prefix("/") {
Some("/")
} else if posix_path.has_prefix("./") {
Some("./")
} else {
None
}
} else if self.is_root(posix_path) {
if posix_path.has_prefix("//") {
Some("//")
} else {
Some("/")
}
} else if posix_path.has_prefix("./") {
Some("./")
} else {
None
}
let rest = match root {
Some(r) => @rb.from(posix_path, r.length())
None => posix_path
}
let segments = @rb.split(rest, "/").filter(s => s != ".")
cache[path] = (segments, root)
(segments.copy(), root)
}
///|
fn join_path(segments : Array[String], root : String?) -> String {
match root {
Some(r) => r + segments.join("/")
None => segments.join("/")
}
}
///|
/// Resolves a system path (Ruby `system_path`), recovering from jail
/// violations with a warning unless `recover` is false.
pub fn PathResolver::system_path(
self : PathResolver,
target : String?,
start? : String,
jail? : String,
target_name? : String = "path",
recover? : Bool = true,
) -> String raise SecurityError {
match jail {
Some(j) =>
if !self.is_root(j) {
raise SecurityError("Jail is not an absolute path: \{j}")
}
None => ()
}
let mut target_segments : Array[String] = []
match target {
Some(t) =>
if self.is_root(t) {
let target_path = self.expand_path(t)
match jail {
Some(j) if self.descends_from(target_path, j) is None => {
if !recover {
raise SecurityError(
"\{target_name} \{t} is outside of jail: \{j} (disallowed in safe mode)",
)
}
log_warn(
"\{target_name} is outside of jail; recovering automatically",
)
let (ts, _) = self.partition_path(target_path)
let (js, jr) = self.partition_path(j)
return join_path(js + ts, jr)
}
_ => return target_path
}
} else {
target_segments = self.partition_path(t).0
}
None => ()
}
let mut start = start
if target_segments.is_empty() {
match start {
None | Some("") =>
return match jail {
Some(j) => j
None => self.working_dir
}
Some(s) =>
if self.is_root(s) {
if jail is None {
return self.expand_path(s)
}
} else {
target_segments = self.partition_path(s).0
start = Some(jail.unwrap_or(self.working_dir))
}
}
} else {
match start {
None | Some("") => start = Some(jail.unwrap_or(self.working_dir))
Some(s) =>
if !self.is_root(s) {
start = Some(
"\{@rb.chomp_suffix(jail.unwrap_or(self.working_dir), "/")}/\{s}",
)
}
}
}
let start = start.unwrap_or("")
let recheck = match jail {
Some(j) => self.descends_from(start, j) is None
None => false
}
let (start_segments, jail_root) = self.partition_path(start)
let mut resolved_segments = start_segments + target_segments
let mut jail_segments : Array[String]? = None
if resolved_segments.contains("..") {
let unresolved = resolved_segments
resolved_segments = []
match jail {
Some(j) => {
let js = self.partition_path(j).0
jail_segments = Some(js)
let mut warned = false
for seg in unresolved {
if seg == ".." {
if resolved_segments.length() > js.length() {
resolved_segments.pop() |> ignore
} else if recover {
if !warned {
log_warn(
"\{target_name} has illegal reference to ancestor of jail; recovering automatically",
)
warned = true
}
} else {
raise SecurityError(
"\{target_name} \{target.unwrap_or("")} refers to location outside jail: \{j} (disallowed in safe mode)",
)
}
} else {
resolved_segments.push(seg)
}
}
}
None =>
for seg in unresolved {
if seg == ".." {
resolved_segments.pop() |> ignore
} else {
resolved_segments.push(seg)
}
}
}
}
if recheck {
let j = jail.unwrap()
let target_path = join_path(resolved_segments, jail_root)
if self.descends_from(target_path, j) is Some(_) {
target_path
} else if recover {
log_warn("\{target_name} is outside of jail; recovering automatically")
let js = match jail_segments {
Some(js) => js
None => self.partition_path(j).0
}
join_path(js + target_segments, jail_root)
} else {
raise SecurityError(
"\{target_name} \{target.unwrap_or("")} is outside of jail: \{j} (disallowed in safe mode)",
)
}
} else {
join_path(resolved_segments, jail_root)
}
}
///|
/// Resolves a web path (Ruby `web_path`).
pub fn PathResolver::web_path(
self : PathResolver,
target : String,
start? : String,
) -> String {
let mut target = target
let mut uri_prefix : String? = None
match start {
Some(s) if s != "" && !self.is_web_root(target) => {
let combined = "\{s}\{if s.has_suffix("/") { "" } else { "/" }}\{target}"
if combined.contains(":") {
match uri_sniff_rx.find(combined) {
Some(m) => {
let matched = m.matched()
target = @rb.from(combined, matched.length())
uri_prefix = Some(matched)
}
None => target = combined
}
} else {
target = combined
}
}
_ => ()
}
let (segments, root) = self.partition_path(target, web=true)
let resolved = []
for seg in segments {
if seg == ".." {
if resolved.is_empty() {
match root {
Some(r) if r != "./" => ()
_ => resolved.push(seg)
}
} else if resolved[resolved.length() - 1] == ".." {
resolved.push(seg)
} else {
resolved.pop() |> ignore
}
} else {
resolved.push(seg)
}
}
let mut resolved_path = join_path(resolved, root)
if resolved_path.contains(" ") {
resolved_path = resolved_path.replace_all(old=" ", new="%20")
}
match uri_prefix {
Some(p) => p + resolved_path
None => resolved_path
}
}