blocxx

PathSecurity.cpp

Go to the documentation of this file.
00001 /*******************************************************************************
00002 * Copyright (C) 2005, Vintela, Inc. All rights reserved.
00003 * Copyright (C) 2006, Novell, Inc. All rights reserved.
00004 * 
00005 * Redistribution and use in source and binary forms, with or without
00006 * modification, are permitted provided that the following conditions are met:
00007 * 
00008 *     * Redistributions of source code must retain the above copyright notice,
00009 *       this list of conditions and the following disclaimer.
00010 *     * Redistributions in binary form must reproduce the above copyright
00011 *       notice, this list of conditions and the following disclaimer in the
00012 *       documentation and/or other materials provided with the distribution.
00013 *     * Neither the name of 
00014 *       Vintela, Inc., 
00015 *       nor Novell, Inc., 
00016 *       nor the names of its contributors or employees may be used to 
00017 *       endorse or promote products derived from this software without 
00018 *       specific prior written permission.
00019 * 
00020 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00021 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00022 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00023 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
00024 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00025 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00026 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00027 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00028 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00029 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00030 * POSSIBILITY OF SUCH DAMAGE.
00031 *******************************************************************************/
00032 
00038 #include "blocxx/BLOCXX_config.h"
00039 #include "blocxx/PathSecurity.hpp"
00040 #include "blocxx/Assertion.hpp"
00041 #include "blocxx/FileSystem.hpp"
00042 #include "blocxx/Format.hpp"
00043 #include "blocxx/String.hpp"
00044 #include "blocxx/Logger.hpp"
00045 #include "blocxx/UserUtils.hpp"
00046 #ifdef BLOCXX_HAVE_UNISTD_H
00047 #include <unistd.h>
00048 #endif
00049 #include <vector>
00050 
00051 namespace BLOCXX_NAMESPACE
00052 {
00053 
00054 typedef Array<std::pair<String, EFileStatusReturn> > path_results_t;
00055 
00056 namespace
00057 {
00058    using namespace FileSystem::Path;
00059 
00060    unsigned const MAX_SYMBOLIC_LINKS = 100;
00061 
00062    // Conceptually, this class consists of two values:
00063    // - A resolved part, which can have path components added or removed
00064    //   at the end, and
00065    // - an unresolved part, which can have path components added or removed
00066    //   at the beginning.
00067    //
00068    class PartiallyResolvedPath
00069    {
00070    public:
00071       // PROMISE: Resolved part is base_dir and unresolved part is empty.
00072       // REQUIRE: base_dir is in canonical form.
00073       PartiallyResolvedPath(char const * base_dir);
00074 
00075       // Prepends the components of path to the unresolved part of the path.
00076       // Note that path may be empty.
00077       // REQUIRE: path does not start with '/'.
00078       void multi_push_unresolved(char const * path);
00079 
00080       // Discards the first component of the unresolved part.
00081       // REQUIRE: unresolved part nonempty.
00082       void pop_unresolved();
00083 
00084       // RETURNS: true iff the unresolved part is empty
00085       bool unresolved_empty() const;
00086 
00087       // RETURNS: true iff the first component of the unresolved part is ".".
00088       bool unresolved_starts_with_curdir() const;
00089 
00090       // RETURNS: true iff the first component of the unresolved part is "..".
00091       bool unresolved_starts_with_parent() const;
00092 
00093       // RETURNS: true iff push_unresolved(path) has ever been called
00094       // with an empty unresolved part and path ending in '/'.
00095       bool dir_specified() const;
00096 
00097       // Transfers the first component of the unresolved part to the end
00098       // of the resolved part.
00099       // REQUIRE: unresolved part is nonempty.
00100       void xfer_component();
00101 
00102       // Discards the last component of the resolved part.
00103       // If resolved part is "/", does nothing (parent of "/" is "/").
00104       void pop_resolved();
00105 
00106       // Resets the resolved part ot "/".
00107       void reset_resolved();
00108 
00109       // RETURNS: the resolved part, as a String.
00110       String get_resolved() const;
00111 
00112       // Calls lstat on the resolved part.
00113       void lstat_resolved(struct stat & st) const;
00114 
00115       // REQUIRE: resolved part is not "/", and last component is a symbolic
00116       // link.
00117       // PROMISE: Reads the symbolic link and assigns it to path (including
00118       // a terminating '\0' character).
00119       void read_symlink(std::vector<char> & path);
00120 
00121    private:
00122 
00123       // INVARIANT: Holds an absolute path with no duplicate '/' chars,
00124       // no "." or ".." components, no component (except possibly the last)
00125       // that is a symlink, and no terminating '/' unless the whole path is
00126       // "/". 
00127       mutable std::vector<char> m_resolved;
00128 
00129       // INVARIANT: holds a relative path with no repeated or terminating '/'
00130       // chars, stored in reverse order.
00131       std::vector<char> m_unresolved;
00132 
00133       bool m_dir_specified;
00134    };
00135 
00136    PartiallyResolvedPath::PartiallyResolvedPath(char const * base_dir)
00137    : m_resolved(base_dir, base_dir + std::strlen(base_dir)),
00138      m_unresolved(),
00139      m_dir_specified(false)
00140    {
00141    }
00142 
00143    void PartiallyResolvedPath::multi_push_unresolved(char const * path)
00144    {
00145       BLOCXX_ASSERT(path && *path != '/');
00146       if (*path == '\0')
00147       {
00148          return;
00149       }
00150       char const * end = path;
00151       while (*end != '\0')
00152       {
00153          ++end;
00154       }
00155       if (end != path && *(end - 1) == '/')
00156       {
00157          m_dir_specified = true;
00158       }
00159       m_unresolved.push_back('/');
00160       bool last_separator = true;
00161       while (end != path)
00162       {
00163          char c = *--end;
00164          bool separator = (c == '/');
00165          if (!(separator && last_separator))
00166          {
00167             m_unresolved.push_back(c);
00168          }
00169          last_separator = separator;
00170       }
00171    }
00172 
00173    void PartiallyResolvedPath::pop_unresolved()
00174    {
00175       BLOCXX_ASSERT(!m_unresolved.empty());
00176       while (!m_unresolved.empty() && m_unresolved.back() != '/')
00177       {
00178          m_unresolved.pop_back();
00179       }
00180       while (!m_unresolved.empty() && m_unresolved.back() == '/')
00181       {
00182          m_unresolved.pop_back();
00183       }
00184    }
00185 
00186    inline bool PartiallyResolvedPath::unresolved_empty() const
00187    {
00188       return m_unresolved.empty();
00189    }
00190 
00191    bool PartiallyResolvedPath::unresolved_starts_with_curdir() const
00192    {
00193       std::size_t n = m_unresolved.size();
00194       return (
00195          n > 0 && m_unresolved[n - 1] == '.' &&
00196          (n == 1 || m_unresolved[n - 2] == '/')
00197       );
00198    }
00199 
00200    bool PartiallyResolvedPath::unresolved_starts_with_parent() const
00201    {
00202       std::size_t n = m_unresolved.size();
00203       return (
00204          n >= 2 && m_unresolved[n - 1] == '.' && m_unresolved[n - 2] == '.'
00205          && (n == 2 || m_unresolved[n - 3] == '/')
00206       );
00207    }
00208 
00209    inline bool PartiallyResolvedPath::dir_specified() const
00210    {
00211       return m_dir_specified;
00212    }
00213 
00214    void PartiallyResolvedPath::xfer_component()
00215    {
00216       BLOCXX_ASSERT(!m_unresolved.empty());
00217       std::size_t n = m_resolved.size();
00218       BLOCXX_ASSERT(n > 0 && (n == 1 || m_resolved[n - 1] != '/'));
00219       if (n > 1)
00220       {
00221          m_resolved.push_back('/');
00222       }
00223       char c;
00224       while (!m_unresolved.empty() && (c = m_unresolved.back()) != '/')
00225       {
00226          m_unresolved.pop_back();
00227          m_resolved.push_back(c);
00228       }
00229       while (!m_unresolved.empty() && m_unresolved.back() == '/')
00230       {
00231          m_unresolved.pop_back();
00232       }
00233    }
00234 
00235    void PartiallyResolvedPath::pop_resolved()
00236    {
00237       std::size_t n = m_resolved.size();
00238       BLOCXX_ASSERT(n > 0 && m_resolved[0] == '/');
00239       if (n == 1)
00240       {
00241          return; // parent of "/" is "/"
00242       }
00243       BLOCXX_ASSERT(m_resolved.back() != '/');
00244       while (m_resolved.back() != '/')
00245       {
00246          m_resolved.pop_back();
00247       }
00248       // pop off path separator too, unless we are back to the root dir
00249       if (m_resolved.size() > 1)
00250       {
00251          m_resolved.pop_back();
00252       }
00253    }
00254 
00255    inline void PartiallyResolvedPath::reset_resolved()
00256    {
00257       std::vector<char>(1, '/').swap(m_resolved);
00258    }
00259 
00260    class NullTerminate
00261    {
00262       std::vector<char> & m_buf;
00263    public:
00264       NullTerminate(std::vector<char> & buf)
00265       : m_buf(buf)
00266       {
00267          m_buf.push_back('\0');
00268       }
00269 
00270       ~NullTerminate()
00271       {
00272          m_buf.pop_back();
00273       }
00274    };
00275 
00276    inline String PartiallyResolvedPath::get_resolved() const
00277    {
00278       NullTerminate x(m_resolved);
00279       return String(&m_resolved[0]);
00280    }
00281 
00282    void wrapped_lstat(char const * path, struct stat & st)
00283    {
00284 #ifdef BLOCXX_WIN32
00285       String tmp_path(path);
00286       if(path[1] == ':' && path[2] == 0)
00287       {
00288          tmp_path += "\\";
00289       }
00290       if (LSTAT(tmp_path.c_str(), &st) < 0)
00291 #else
00292       if (LSTAT(path, &st) < 0)
00293 #endif
00294       {
00295          BLOCXX_THROW_ERRNO_MSG(FileSystemException, path);
00296       }
00297    }
00298 
00299    void PartiallyResolvedPath::lstat_resolved(struct stat & st) const
00300    {
00301       NullTerminate x(m_resolved);
00302       wrapped_lstat(&m_resolved[0], st);
00303    }
00304 
00305    void PartiallyResolvedPath::read_symlink(std::vector<char> & path)
00306    {
00307       BLOCXX_ASSERT(READLINK_ALLOWED);
00308       NullTerminate x(m_resolved);
00309       std::vector<char> buf(MAXPATHLEN + 1);
00310       while (true)
00311       {
00312          char const * symlink_path = &m_resolved[0];
00313          int rv = READLINK(symlink_path, &buf[0], buf.size());
00314          // Note that if the link value is too big to fit into buf, but
00315          // there is no other error, then rv == buf.size(); in particular,
00316          // we do NOT get rv < 0 with errno == ENAMETOOLONG (this refers
00317          // to the input path, not the link value returned).
00318          if (rv < 0)
00319          {
00320             BLOCXX_THROW_ERRNO_MSG(FileSystemException, symlink_path);
00321          }
00322          else if (static_cast<unsigned>(rv) == buf.size())
00323          {
00324             buf.resize(2 * buf.size());
00325          }
00326          else
00327          {
00328             path.swap(buf);
00329             return;
00330          }
00331       }
00332    }
00333 
00334    char const * strip_leading_slashes(char const * path)
00335    {
00336       while (*path == '/')
00337       {
00338          ++path;
00339       }
00340       return path;
00341    }
00342 
00343    void logFileStatus(path_results_t const & results, const uid_t uid)
00344    {
00345       Logger logger("blocxx.PathSecurity");
00346       for (path_results_t::const_iterator li = results.begin(); li != results.end(); ++li)
00347       {
00348          switch (li->second)
00349          {
00350             case E_FILE_BAD_OWNER:
00351                {
00352                   String hpux;
00353 
00354                   bool successful(false);
00355                   String userName = UserUtils::getUserName(uid, successful);
00356                   if (!successful)
00357                   {
00358                      userName = "the proper user";
00359                   }
00360 #if defined(BLOCXX_HPUX) || defined(BLOCXX_AIX)
00361                   else
00362                   {
00363                      if (uid == 0)
00364                      {
00365                         hpux = " (or bin)";
00366                      }
00367                   }
00368 #endif
00369 
00370                   BLOCXX_LOG_ERROR(logger, Format("%1 was insecure.  It was not owned by %2%3.", li->first, userName, hpux));
00371                   break;
00372                }
00373             case E_FILE_BAD_OTHER:
00374                {
00375 
00376                   BLOCXX_LOG_ERROR(logger, Format("%1 was owned by the proper user, but was not a symlink and was either"
00377                      " world (or non-root group) writable or did not have the sticky bit set on the directory.", li->first));
00378                   break;
00379                }
00380             default:
00381                break;
00382          }
00383       }
00384    }
00385 
00386    std::pair<ESecurity, String>
00387    path_security(char const * base_dir, char const * rel_path, uid_t uid, bool bdsecure)
00388    {
00389       BLOCXX_ASSERT(base_dir[0] == '/');
00390       BLOCXX_ASSERT(
00391          base_dir[1] == '\0' || base_dir[std::strlen(base_dir) - 1] != '/');
00392       BLOCXX_ASSERT(rel_path[0] != '/');
00393 
00394       struct stat st;
00395 
00396 #ifndef BLOCXX_WIN32
00397       PartiallyResolvedPath prp(base_dir);
00398 #else
00399       PartiallyResolvedPath prp("");
00400       if (rel_path[1] != ':' && base_dir[1] == ':')
00401       {
00402          prp = base_dir;
00403       }
00404 #endif
00405       ESecurity status_if_secure = E_SECURE_DIR;
00406       unsigned num_symbolic_links = 0;
00407       EFileStatusReturn file_status(E_FILE_BAD_OTHER);
00408       path_results_t results;
00409 
00410       prp.multi_push_unresolved(rel_path);
00411       // This handles the case where there are no unresolved items in the path (only possible for '/')
00412       if( prp.unresolved_empty() )
00413       {
00414          prp.lstat_resolved(st);
00415          file_status = getFileStatus(st, uid, true, rel_path);
00416       }
00417       while (!prp.unresolved_empty())
00418       {
00419          if (prp.unresolved_starts_with_curdir())
00420          {
00421             prp.pop_unresolved();
00422          }
00423          else if (prp.unresolved_starts_with_parent())
00424          {
00425             prp.pop_unresolved();
00426             prp.pop_resolved();
00427          }
00428          else
00429          {
00430             prp.xfer_component();
00431             prp.lstat_resolved(st);
00432             file_status = getFileStatus(st, uid, prp.unresolved_empty(), prp.get_resolved());
00433 
00434             if (file_status != E_FILE_OK)
00435             {
00436                results.push_back(std::make_pair(prp.get_resolved(), file_status));
00437             }
00438 
00439             if (S_ISREG(st.st_mode))
00440             {
00441                status_if_secure = E_SECURE_FILE;
00442                if (!prp.unresolved_empty() || prp.dir_specified())
00443                {
00444                   BLOCXX_THROW_ERRNO_MSG1(
00445                      FileSystemException, prp.get_resolved(), ENOTDIR);
00446                }
00447             }
00448             else if (S_ISLNK(st.st_mode))
00449             {
00450                if (++num_symbolic_links > MAX_SYMBOLIC_LINKS)
00451                {
00452                   BLOCXX_THROW_ERRNO_MSG1(
00453                      FileSystemException, prp.get_resolved(), ELOOP);
00454                }
00455                std::vector<char> slpath_vec;
00456                prp.read_symlink(slpath_vec);
00457                char const * slpath = &slpath_vec[0];
00458                if (slpath[0] == '/')
00459                {
00460                   prp.reset_resolved();
00461                   slpath = strip_leading_slashes(slpath);
00462                }
00463                else
00464                {
00465                   prp.pop_resolved();
00466                }
00467                prp.multi_push_unresolved(slpath);
00468             }
00469             else if (!S_ISDIR(st.st_mode))
00470             {
00471                String msg = prp.get_resolved() + 
00472                   " is not a directory, symbolic link, nor regular file";
00473                BLOCXX_THROW(FileSystemException, msg.c_str());
00474             }
00475          }
00476       }
00477 
00478       ESecurity sec = (bdsecure && file_status == E_FILE_OK) ? status_if_secure : E_INSECURE;
00479       logFileStatus(results, uid);
00480       return std::make_pair(sec, prp.get_resolved());
00481    }
00482 
00483    std::pair<ESecurity, String> path_security(char const * path, UserId uid)
00484    {
00485       if(!isPathAbsolute(path))
00486       {
00487          BLOCXX_THROW(FileSystemException,
00488             Format("%1 is not an absolute path", path).c_str());
00489       }
00490       char const * relpath = strip_leading_slashes(path);
00491       struct stat st;
00492 #ifndef BLOCXX_WIN32
00493       char sRootPath[] = "/";
00494       wrapped_lstat("/", st);
00495 #else
00496       char sRootPath[MAX_PATH];
00497       if (::GetWindowsDirectory(sRootPath, MAX_PATH) < 3 )  // we need at least 3 symbols 
00498       {
00499          wrapped_lstat("/", st);
00500       }
00501       else
00502       {
00503          sRootPath[3] = 0; // we're interesting only for len('X:/')=3 letters
00504          wrapped_lstat(sRootPath, st);
00505       }
00506 #endif
00507       EFileStatusReturn file_status = getFileStatus(st, uid, *relpath == '\0', path);
00508 
00509       path_results_t results;
00510       if (file_status != E_FILE_OK)
00511       {
00512          results.push_back(std::make_pair(String(path), file_status));
00513          logFileStatus(results, uid);
00514       }
00515 
00516       return path_security(sRootPath, relpath, uid, (file_status == E_FILE_OK));
00517    }
00518 
00519 } // anonymous namespace
00520 
00521 std::pair<ESecurity, String> 
00522 FileSystem::Path::security(String const & path, UserId uid)
00523 {
00524    String abspath = isPathAbsolute(path) ? path : getCurrentWorkingDirectory() + BLOCXX_FILENAME_SEPARATOR + path;
00525    return path_security(abspath.c_str(), uid);
00526 }
00527 
00528 std::pair<ESecurity, String>
00529 FileSystem::Path::security(
00530    String const & base_dir, String const & rel_path, UserId uid)
00531 {
00532    return path_security(base_dir.c_str(), rel_path.c_str(), uid, true);
00533 }
00534 
00535 std::pair<ESecurity, String> 
00536 FileSystem::Path::security(String const & path)
00537 {
00538    return security(path, ::geteuid());
00539 }
00540 
00541 std::pair<ESecurity, String>
00542 FileSystem::Path::security(
00543    String const & base_dir, String const & rel_path)
00544 {
00545    return security(base_dir, rel_path, ::geteuid());
00546 }
00547 
00548 } // end namespace BLOCXX_NAMESPACE
00549