Class: SafePath

Inherits:
Object
  • Object
show all
Defined in:
lib/ndr_support/safe_path.rb

Overview

SafePath

SafePath is a class which contains path to a file or directory. It also holds “path space” and permissions. The path space is a directory. Everything in this directory and all the subdirectories can be accessed with the permissions given to the constructor. The instance of the class checks whether the path constructed points to a directory, whcih is in the “path space”. The idea is to limit the access of the program to given directory.

Example of usage is :

sp = SafePath("dbs_inbox")

The root directory of the pathspace is in the file config/filesystem_paths.yml . In this case dbs_inbox has root /mounts/ron/dbs_inbox . Every path which starts with /mount/ron/dbs_inbox is considered as safe. If a path is constructed which is /mounts/ron/dbs_inbox/../../../etc/passwd for example then the class will evaluate the path and it will raise exception SecurityError.

The paths can be constructed by using +, join or join!. Example:

This:
  sp = SafePath("dbs_inbox")
  sp + "/my_dir"
Points to:
  /mounts/ron/dbs_inbox/my_dir

The functions join and join! work in similar way. The difference between join and join! is that join creates new instance of the class SafePath and return it and join! doesn’t create new instance, but works in-place and after that it returns reference to the current instance. The both operators can be used like that:

sp.join("/my_dir")  #this is the same as sp + "my_dir"
sp.join!("/my_dir") #this is NOT the same as sp "my_dir"

Warning the function sp.path = “some_path” will treat some_path as absolute path and if it doesn’t point to the root it will raise exception. The danger is that it returns the argument on the right hand side. So if it is a string the operator will return a string. This is the way ruby works. If it is used properly it shouldn’t be a problem. The best way to use it is:

sp.path = sp.root + "my_dir"

sp.root returns SafePath and after that + is called which also returns SafePath. So the right hand side of the expression is SafePath and the = will return SafePath.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path_space, root_suffix = '') ⇒ SafePath

Takes:

 * path - This is a path to a directory. Usually a string.
 * path_space - This is identifier of the path space in whichi the system should work.
   it is a string. To find list of path spaces with their roots, please
   see config/filesystem_paths.yml

Raises:
  * ArgumentError
  * SecurityError


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/ndr_support/safe_path.rb', line 86

def initialize(path_space, root_suffix = '')
  # The class has to use different path definitions during test

  fs_paths = self.class.fs_paths

  platform = fs_paths.keys.select do |key|
    RUBY_PLATFORM.match key
  end[0]

  root_path_regexp = if platform == /mswin32|mingw32/
                      /\A([A-Z]:[\\\/]|\/\/)/
                     else
                      /\A\//
                     end

  fail ArgumentError, "The space #{path_space} doesn't exist. Please choose one of: #{fs_paths[platform].keys.inspect}" unless fs_paths[platform][path_space]
  fail ArgumentError, "The space #{path_space} is broken. The root path should be absolute path but it was #{fs_paths[platform][path_space]['root']}" unless fs_paths[platform][path_space]['root'].match(root_path_regexp)
  fail ArgumentError, "The space #{path_space} is broken. No permissions specified}" unless fs_paths[platform][path_space]['prms']

  # The function verify uses @root. Therefore first assign the root path
  # specified in the yaml file. After that verify that the root path
  # specified from the contructor is subpath of the path specified in the
  # yaml file. If it is not raise exception before anything else is done.
  #
  # The reason to assign @root 2 times is that it is better to have the
  # logic verifing the path in only one function. This way it is going
  # to be easier to maintain it and keep it secure.
  #

  @root = File.expand_path fs_paths[platform][path_space]['root']
  @root = verify(File.join(fs_paths[platform][path_space]['root'], root_suffix)) unless root_suffix.blank?

  @path_space = path_space
  @maximum_prms = fs_paths[platform][path_space]['prms']
  @prm = nil

  self.path = @root
end

Class Method Details

.configure!(filepath) ⇒ Object

Takes the path the filesystem_paths.yml file that should be used. Attempting to reconfigure with new settings will raise a security error.

Raises:

  • (SecurityError)


66
67
68
69
70
71
72
73
74
# File 'lib/ndr_support/safe_path.rb', line 66

def self.configure!(filepath)
  raise SecurityError, 'Attempt to re-assign SafePath config!' if defined?(@@fs_paths)

  File.open(filepath, 'r') do |file|
    @@fs_paths = YAML.safe_load(ERB.new(file.read).result,
                                permitted_classes: [Regexp], aliases: true)
    @@fs_paths.freeze
  end
end

.fs_pathsObject

Returns the list of safe ‘root’ filesystem locations, or raises a SecurityError if no configuration has been provided.



55
56
57
58
59
60
61
# File 'lib/ndr_support/safe_path.rb', line 55

def self.fs_paths
  if defined?(@@fs_paths)
    @@fs_paths
  else
    fail SecurityError, 'SafePath not configured!'
  end
end

Instance Method Details

#+(path) ⇒ Object

Another name for join



223
224
225
# File 'lib/ndr_support/safe_path.rb', line 223

def +(path)
  self.join(path)
end

#==(other) ⇒ Object



125
126
127
# File 'lib/ndr_support/safe_path.rb', line 125

def ==(other)
  other.class == SafePath and other.root.to_s == @root and other.permissions == self.permissions and other.to_s == self.to_s
end

#join(path) ⇒ Object

Used to construct path. It joins the current path with the given one. Takes:

* path - path to be concatenated

Returns:

New instance of SafePath which contains the new path.

Raises:

* SecurityError


237
238
239
240
241
# File 'lib/ndr_support/safe_path.rb', line 237

def join(path)
  r = self.clone
  r.path = File.join(@path, path)
  r
end

#join!(path) ⇒ Object

Used to construct path. It joins the current path with the given one. Takes:

* path - path to be concatenated

Returns:

Reference to the current instance. It works in-place

Raises:

* SecurityError


253
254
255
256
# File 'lib/ndr_support/safe_path.rb', line 253

def join!(path)
  self.path = File.join(@path, path)
  self
end

#lengthObject



258
259
260
# File 'lib/ndr_support/safe_path.rb', line 258

def length()
  @path.length
end

#path=(path) ⇒ Object

Setter for path.

Takes:

* path - The path.

Returns:

Array - this is the result of the assignment. Note it is the right hand side of the expression

Raises:

* SecurityError

Warning avoid using this in expressions like this (safe_path_new = (safe_path.path = safe_path.root.to_s + “/test”)) + path_from_attacker This is unsafe!



217
218
219
220
# File 'lib/ndr_support/safe_path.rb', line 217

def path=(path)
  @path = verify(path)
  self
end

#path_spaceObject

Getter for path space identifier.

Returns:

Array


164
165
166
# File 'lib/ndr_support/safe_path.rb', line 164

def path_space
  @path_space
end

#permissionsObject

Getter for permissions.

Returns:

Array


151
152
153
154
155
156
157
# File 'lib/ndr_support/safe_path.rb', line 151

def permissions
  if @prm
    @prm
  else
    @maximum_prms
  end
end

#permissions=(permissions) ⇒ Object

The permissions are specified in the yml file, but this function can select subset of these permissions. It cannot select permission, which is not specified in the yml file.

Takes:

* Array of permission or single permission - If it is array then
  tha array could contain duplicates and it also can be nested.
  All the duplicates will be removed and the array will be flattened

Returns:

Array - this is the reuslt of the assignment. Note it is the right hand side of the expression

Raises:

* ArgumentError


195
196
197
198
199
200
201
# File 'lib/ndr_support/safe_path.rb', line 195

def permissions=(permissions)
  err_mess = "permissions has to be one or more of the values: #{@maximum_prms.inspect}\n but it was #{permissions.inspect}"

  @prm = [permissions].flatten.each do |prm|
    fail ArgumentError, err_mess unless @maximum_prms.include?(prm)
  end.uniq
end

#rootObject

Getter for the root of the path space.

Returns:

SafePath


173
174
175
176
177
178
# File 'lib/ndr_support/safe_path.rb', line 173

def root
  r = self.clone
  r.path = @root
  r.permissions = self.permissions
  r
end

#to_sObject

WARNING: do not use sp.to_s + from_attacker . This is unsafe!

Returns:

String


134
135
136
# File 'lib/ndr_support/safe_path.rb', line 134

def to_s
  @path
end

#to_strObject

WARNING: do not use s.to_s + “my_path” . This is unsafe!

Returns:
  String


143
144
145
# File 'lib/ndr_support/safe_path.rb', line 143

def to_str
  self.to_s
end