blocxx
PathSecurity.cpp
Go to the documentation of this file.
1 /*******************************************************************************
2 * Copyright (C) 2005, Vintela, Inc. All rights reserved.
3 * Copyright (C) 2006, Novell, Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * * Neither the name of
14 * Vintela, Inc.,
15 * nor Novell, Inc.,
16 * nor the names of its contributors or employees may be used to
17 * endorse or promote products derived from this software without
18 * specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 * POSSIBILITY OF SUCH DAMAGE.
31 *******************************************************************************/
32 
38 #include "blocxx/BLOCXX_config.h"
39 #include "blocxx/PathSecurity.hpp"
40 #include "blocxx/Assertion.hpp"
41 #include "blocxx/FileSystem.hpp"
42 #include "blocxx/Format.hpp"
43 #include "blocxx/String.hpp"
44 #include "blocxx/Logger.hpp"
45 #include "blocxx/UserUtils.hpp"
46 #ifdef BLOCXX_HAVE_UNISTD_H
47 #include <unistd.h>
48 #endif
49 #include <vector>
50 
51 namespace BLOCXX_NAMESPACE
52 {
53 
55 
56 namespace
57 {
58  using namespace FileSystem::Path;
59 
60  unsigned const MAX_SYMBOLIC_LINKS = 100;
61 
62  // Conceptually, this class consists of two values:
63  // - A resolved part, which can have path components added or removed
64  // at the end, and
65  // - an unresolved part, which can have path components added or removed
66  // at the beginning.
67  //
68  class PartiallyResolvedPath
69  {
70  public:
71  // PROMISE: Resolved part is base_dir and unresolved part is empty.
72  // REQUIRE: base_dir is in canonical form.
73  PartiallyResolvedPath(char const * base_dir);
74 
75  // Prepends the components of path to the unresolved part of the path.
76  // Note that path may be empty.
77  // REQUIRE: path does not start with '/'.
78  void multi_push_unresolved(char const * path);
79 
80  // Discards the first component of the unresolved part.
81  // REQUIRE: unresolved part nonempty.
82  void pop_unresolved();
83 
84  // RETURNS: true iff the unresolved part is empty
85  bool unresolved_empty() const;
86 
87  // RETURNS: true iff the first component of the unresolved part is ".".
88  bool unresolved_starts_with_curdir() const;
89 
90  // RETURNS: true iff the first component of the unresolved part is "..".
91  bool unresolved_starts_with_parent() const;
92 
93  // RETURNS: true iff push_unresolved(path) has ever been called
94  // with an empty unresolved part and path ending in '/'.
95  bool dir_specified() const;
96 
97  // Transfers the first component of the unresolved part to the end
98  // of the resolved part.
99  // REQUIRE: unresolved part is nonempty.
100  void xfer_component();
101 
102  // Discards the last component of the resolved part.
103  // If resolved part is "/", does nothing (parent of "/" is "/").
104  void pop_resolved();
105 
106  // Resets the resolved part ot "/".
107  void reset_resolved();
108 
109  // RETURNS: the resolved part, as a String.
110  String get_resolved() const;
111 
112  // Calls lstat on the resolved part.
113  void lstat_resolved(struct stat & st) const;
114 
115  // REQUIRE: resolved part is not "/", and last component is a symbolic
116  // link.
117  // PROMISE: Reads the symbolic link and assigns it to path (including
118  // a terminating '\0' character).
119  void read_symlink(std::vector<char> & path);
120 
121  private:
122 
123  // INVARIANT: Holds an absolute path with no duplicate '/' chars,
124  // no "." or ".." components, no component (except possibly the last)
125  // that is a symlink, and no terminating '/' unless the whole path is
126  // "/".
127  mutable std::vector<char> m_resolved;
128 
129  // INVARIANT: holds a relative path with no repeated or terminating '/'
130  // chars, stored in reverse order.
131  std::vector<char> m_unresolved;
132 
134  };
135 
136  PartiallyResolvedPath::PartiallyResolvedPath(char const * base_dir)
137  : m_resolved(base_dir, base_dir + std::strlen(base_dir)),
138  m_unresolved(),
139  m_dir_specified(false)
140  {
141  }
142 
143  void PartiallyResolvedPath::multi_push_unresolved(char const * path)
144  {
145  BLOCXX_ASSERT(path && *path != '/');
146  if (*path == '\0')
147  {
148  return;
149  }
150  char const * end = path;
151  while (*end != '\0')
152  {
153  ++end;
154  }
155  if (end != path && *(end - 1) == '/')
156  {
157  m_dir_specified = true;
158  }
159  m_unresolved.push_back('/');
160  bool last_separator = true;
161  while (end != path)
162  {
163  char c = *--end;
164  bool separator = (c == '/');
165  if (!(separator && last_separator))
166  {
167  m_unresolved.push_back(c);
168  }
169  last_separator = separator;
170  }
171  }
172 
173  void PartiallyResolvedPath::pop_unresolved()
174  {
175  BLOCXX_ASSERT(!m_unresolved.empty());
176  while (!m_unresolved.empty() && m_unresolved.back() != '/')
177  {
178  m_unresolved.pop_back();
179  }
180  while (!m_unresolved.empty() && m_unresolved.back() == '/')
181  {
182  m_unresolved.pop_back();
183  }
184  }
185 
186  inline bool PartiallyResolvedPath::unresolved_empty() const
187  {
188  return m_unresolved.empty();
189  }
190 
191  bool PartiallyResolvedPath::unresolved_starts_with_curdir() const
192  {
193  std::size_t n = m_unresolved.size();
194  return (
195  n > 0 && m_unresolved[n - 1] == '.' &&
196  (n == 1 || m_unresolved[n - 2] == '/')
197  );
198  }
199 
200  bool PartiallyResolvedPath::unresolved_starts_with_parent() const
201  {
202  std::size_t n = m_unresolved.size();
203  return (
204  n >= 2 && m_unresolved[n - 1] == '.' && m_unresolved[n - 2] == '.'
205  && (n == 2 || m_unresolved[n - 3] == '/')
206  );
207  }
208 
209  inline bool PartiallyResolvedPath::dir_specified() const
210  {
211  return m_dir_specified;
212  }
213 
214  void PartiallyResolvedPath::xfer_component()
215  {
216  BLOCXX_ASSERT(!m_unresolved.empty());
217  std::size_t n = m_resolved.size();
218  BLOCXX_ASSERT(n > 0 && (n == 1 || m_resolved[n - 1] != '/'));
219  if (n > 1)
220  {
221  m_resolved.push_back('/');
222  }
223  char c;
224  while (!m_unresolved.empty() && (c = m_unresolved.back()) != '/')
225  {
226  m_unresolved.pop_back();
227  m_resolved.push_back(c);
228  }
229  while (!m_unresolved.empty() && m_unresolved.back() == '/')
230  {
231  m_unresolved.pop_back();
232  }
233  }
234 
235  void PartiallyResolvedPath::pop_resolved()
236  {
237  std::size_t n = m_resolved.size();
238  BLOCXX_ASSERT(n > 0 && m_resolved[0] == '/');
239  if (n == 1)
240  {
241  return; // parent of "/" is "/"
242  }
243  BLOCXX_ASSERT(m_resolved.back() != '/');
244  while (m_resolved.back() != '/')
245  {
246  m_resolved.pop_back();
247  }
248  // pop off path separator too, unless we are back to the root dir
249  if (m_resolved.size() > 1)
250  {
251  m_resolved.pop_back();
252  }
253  }
254 
255  inline void PartiallyResolvedPath::reset_resolved()
256  {
257  std::vector<char>(1, '/').swap(m_resolved);
258  }
259 
260  class NullTerminate
261  {
262  std::vector<char> & m_buf;
263  public:
264  NullTerminate(std::vector<char> & buf)
265  : m_buf(buf)
266  {
267  m_buf.push_back('\0');
268  }
269 
270  ~NullTerminate()
271  {
272  m_buf.pop_back();
273  }
274  };
275 
276  inline String PartiallyResolvedPath::get_resolved() const
277  {
278  NullTerminate x(m_resolved);
279  return String(&m_resolved[0]);
280  }
281 
282  void wrapped_lstat(char const * path, struct stat & st)
283  {
284 #ifdef BLOCXX_WIN32
285  String tmp_path(path);
286  if(path[1] == ':' && path[2] == 0)
287  {
288  tmp_path += "\\";
289  }
290  if (LSTAT(tmp_path.c_str(), &st) < 0)
291 #else
292  if (LSTAT(path, &st) < 0)
293 #endif
294  {
295  BLOCXX_THROW_ERRNO_MSG(FileSystemException, path);
296  }
297  }
298 
299  void PartiallyResolvedPath::lstat_resolved(struct stat & st) const
300  {
301  NullTerminate x(m_resolved);
302  wrapped_lstat(&m_resolved[0], st);
303  }
304 
305  void PartiallyResolvedPath::read_symlink(std::vector<char> & path)
306  {
308  NullTerminate x(m_resolved);
309  std::vector<char> buf(MAXPATHLEN + 1);
310  while (true)
311  {
312  char const * symlink_path = &m_resolved[0];
313  int rv = READLINK(symlink_path, &buf[0], buf.size());
314  // Note that if the link value is too big to fit into buf, but
315  // there is no other error, then rv == buf.size(); in particular,
316  // we do NOT get rv < 0 with errno == ENAMETOOLONG (this refers
317  // to the input path, not the link value returned).
318  if (rv < 0)
319  {
320  BLOCXX_THROW_ERRNO_MSG(FileSystemException, symlink_path);
321  }
322  else if (static_cast<unsigned>(rv) == buf.size())
323  {
324  buf.resize(2 * buf.size());
325  }
326  else
327  {
328  path.swap(buf);
329  return;
330  }
331  }
332  }
333 
334  char const * strip_leading_slashes(char const * path)
335  {
336  while (*path == '/')
337  {
338  ++path;
339  }
340  return path;
341  }
342 
343  void logFileStatus(path_results_t const & results, const uid_t uid)
344  {
345  Logger logger("blocxx.PathSecurity");
346  for (path_results_t::const_iterator li = results.begin(); li != results.end(); ++li)
347  {
348  switch (li->second)
349  {
350  case E_FILE_BAD_OWNER:
351  {
352  String hpux;
353 
354  bool successful(false);
355  String userName = UserUtils::getUserName(uid, successful);
356  if (!successful)
357  {
358  userName = "the proper user";
359  }
360 #if defined(BLOCXX_HPUX) || defined(BLOCXX_AIX)
361  else
362  {
363  if (uid == 0)
364  {
365  hpux = " (or bin)";
366  }
367  }
368 #endif
369 
370  BLOCXX_LOG_ERROR(logger, Format("%1 was insecure. It was not owned by %2%3.", li->first, userName, hpux));
371  break;
372  }
373  case E_FILE_BAD_OTHER:
374  {
375 
376  BLOCXX_LOG_ERROR(logger, Format("%1 was owned by the proper user, but was not a symlink and was either"
377  " world (or non-root group) writable or did not have the sticky bit set on the directory.", li->first));
378  break;
379  }
380  default:
381  break;
382  }
383  }
384  }
385 
386  std::pair<ESecurity, String>
387  path_security(char const * base_dir, char const * rel_path, uid_t uid, bool bdsecure)
388  {
389  BLOCXX_ASSERT(base_dir[0] == '/');
391  base_dir[1] == '\0' || base_dir[std::strlen(base_dir) - 1] != '/');
392  BLOCXX_ASSERT(rel_path[0] != '/');
393 
394  struct stat st;
395 
396 #ifndef BLOCXX_WIN32
397  PartiallyResolvedPath prp(base_dir);
398 #else
399  PartiallyResolvedPath prp("");
400  if (rel_path[1] != ':' && base_dir[1] == ':')
401  {
402  prp = base_dir;
403  }
404 #endif
405  ESecurity status_if_secure = E_SECURE_DIR;
406  unsigned num_symbolic_links = 0;
407  EFileStatusReturn file_status(E_FILE_BAD_OTHER);
408  path_results_t results;
409 
410  prp.multi_push_unresolved(rel_path);
411  // This handles the case where there are no unresolved items in the path (only possible for '/')
412  if( prp.unresolved_empty() )
413  {
414  prp.lstat_resolved(st);
415  file_status = getFileStatus(st, uid, true, rel_path);
416  }
417  while (!prp.unresolved_empty())
418  {
419  if (prp.unresolved_starts_with_curdir())
420  {
421  prp.pop_unresolved();
422  }
423  else if (prp.unresolved_starts_with_parent())
424  {
425  prp.pop_unresolved();
426  prp.pop_resolved();
427  }
428  else
429  {
430  prp.xfer_component();
431  prp.lstat_resolved(st);
432  file_status = getFileStatus(st, uid, prp.unresolved_empty(), prp.get_resolved());
433 
434  if (file_status != E_FILE_OK)
435  {
436  results.push_back(std::make_pair(prp.get_resolved(), file_status));
437  }
438 
439  if (S_ISREG(st.st_mode))
440  {
441  status_if_secure = E_SECURE_FILE;
442  if (!prp.unresolved_empty() || prp.dir_specified())
443  {
445  FileSystemException, prp.get_resolved(), ENOTDIR);
446  }
447  }
448  else if (S_ISLNK(st.st_mode))
449  {
450  if (++num_symbolic_links > MAX_SYMBOLIC_LINKS)
451  {
453  FileSystemException, prp.get_resolved(), ELOOP);
454  }
455  std::vector<char> slpath_vec;
456  prp.read_symlink(slpath_vec);
457  char const * slpath = &slpath_vec[0];
458  if (slpath[0] == '/')
459  {
460  prp.reset_resolved();
461  slpath = strip_leading_slashes(slpath);
462  }
463  else
464  {
465  prp.pop_resolved();
466  }
467  prp.multi_push_unresolved(slpath);
468  }
469  else if (!S_ISDIR(st.st_mode))
470  {
471  String msg = prp.get_resolved() +
472  " is not a directory, symbolic link, nor regular file";
473  BLOCXX_THROW(FileSystemException, msg.c_str());
474  }
475  }
476  }
477 
478  ESecurity sec = (bdsecure && file_status == E_FILE_OK) ? status_if_secure : E_INSECURE;
479  logFileStatus(results, uid);
480  return std::make_pair(sec, prp.get_resolved());
481  }
482 
483  std::pair<ESecurity, String> path_security(char const * path, UserId uid)
484  {
485  if(!isPathAbsolute(path))
486  {
487  BLOCXX_THROW(FileSystemException,
488  Format("%1 is not an absolute path", path).c_str());
489  }
490  char const * relpath = strip_leading_slashes(path);
491  struct stat st;
492 #ifndef BLOCXX_WIN32
493  char sRootPath[] = "/";
494  wrapped_lstat("/", st);
495 #else
496  char sRootPath[MAX_PATH];
497  if (::GetWindowsDirectory(sRootPath, MAX_PATH) < 3 ) // we need at least 3 symbols
498  {
499  wrapped_lstat("/", st);
500  }
501  else
502  {
503  sRootPath[3] = 0; // we're interesting only for len('X:/')=3 letters
504  wrapped_lstat(sRootPath, st);
505  }
506 #endif
507  EFileStatusReturn file_status = getFileStatus(st, uid, *relpath == '\0', path);
508 
509  path_results_t results;
510  if (file_status != E_FILE_OK)
511  {
512  results.push_back(std::make_pair(String(path), file_status));
513  logFileStatus(results, uid);
514  }
515 
516  return path_security(sRootPath, relpath, uid, (file_status == E_FILE_OK));
517  }
518 
519 } // anonymous namespace
520 
521 std::pair<ESecurity, String>
523 {
525  return path_security(abspath.c_str(), uid);
526 }
527 
528 std::pair<ESecurity, String>
530  String const & base_dir, String const & rel_path, UserId uid)
531 {
532  return path_security(base_dir.c_str(), rel_path.c_str(), uid, true);
533 }
534 
535 std::pair<ESecurity, String>
537 {
538  return security(path, ::geteuid());
539 }
540 
541 std::pair<ESecurity, String>
543  String const & base_dir, String const & rel_path)
544 {
545  return security(base_dir, rel_path, ::geteuid());
546 }
547 
548 } // end namespace BLOCXX_NAMESPACE
549