Module: Mbeditor::SafePath
- Defined in:
- app/services/mbeditor/safe_path.rb
Overview
Containment check for paths that may not exist yet.
File.realpath refuses to resolve a path whose final component is missing,
so callers that create files have to reason about the nearest existing
ancestor instead. Doing that with File.exist? is unsafe: it follows
symlinks, so a symlink pointing at a missing target reports false and an
existence walk steps straight past it, back to the safely-contained parent.
A subsequent File.open follows the link and writes outside the root.
SafePath resolves the link chain itself with +File.symlink?+/+File.readlink+ (both lstat-based, neither follows) so a dangling symlink is judged by where it points, not by whether that target happens to exist yet.
Constant Summary collapse
- MAX_SYMLINK_HOPS =
Guards against symlink cycles, which surface as a path that neither exists nor terminates.
32
Class Method Summary collapse
-
.canonical(path, hops = 0) ⇒ Object
The filesystem location a write to
pathwould actually land on, with all symlinks expanded. -
.within?(root, path) ⇒ Boolean
True when
pathresolves to a location insideroot, following every symlink component including dangling ones.
Class Method Details
.canonical(path, hops = 0) ⇒ Object
The filesystem location a write to path would actually land on, with
all symlinks expanded. Returns nil when the link chain cycles or exceeds
MAX_SYMLINK_HOPS.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'app/services/mbeditor/safe_path.rb', line 38 def canonical(path, hops = 0) return nil if hops > MAX_SYMLINK_HOPS # Anything that exists (including a symlink with a live target) resolves # normally; realpath expands the whole chain for us. return File.realpath(path) if File.exist?(path) parent = File.dirname(path) return nil if parent == path # ran off the top without finding anything real_parent = canonical(parent, hops) return nil unless real_parent if File.symlink?(path) canonical(File.absolute_path(File.readlink(path), real_parent), hops + 1) else File.join(real_parent, File.basename(path)) end end |
.within?(root, path) ⇒ Boolean
True when path resolves to a location inside root, following every
symlink component including dangling ones. root must exist.
25 26 27 28 29 30 31 32 33 |
# File 'app/services/mbeditor/safe_path.rb', line 25 def within?(root, path) real_root = File.realpath(root.to_s) target = canonical(path.to_s) return false unless target target == real_root || target.start_with?("#{real_root}/") rescue SystemCallError false end |