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
37
38#include "blocxx/BLOCXX_config.h"
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
51namespace BLOCXX_NAMESPACE
52{
53
55
56namespace
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
133 bool m_dir_specified;
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 {
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 {
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;
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";
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 {
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
521std::pair<ESecurity, String>
523{
525 return path_security(abspath.c_str(), uid);
526}
527
528std::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
535std::pair<ESecurity, String>
537{
538 return security(path, ::geteuid());
539}
540
541std::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
#define BLOCXX_ASSERT(CON)
BLOCXX_ASSERT works similar to the assert() macro, but instead of calling abort(),...
Definition Assertion.hpp:57
#define BLOCXX_THROW(exType, msg)
Throw an exception using FILE and LINE.
#define BLOCXX_THROW_ERRNO_MSG(exType, msg)
Throw an exception using FILE, LINE, errno and strerror(errno)
#define BLOCXX_THROW_ERRNO_MSG1(exType, msg, errnum)
Throw an exception using FILE, LINE, errnum and strerror(errnum)
#define MAXPATHLEN
#define BLOCXX_LOG_ERROR(logger, message)
Log message to logger with the Error level.
Definition Logger.hpp:433
#define READLINK_ALLOWED
#define LSTAT
#define READLINK(path, buf, size)
#define BLOCXX_FILENAME_SEPARATOR
Definition Types.hpp:153
Array<> wraps std::vector<> in COWReference<> adding ref counting and copy on write capability.
Definition ArrayFwd.hpp:46
void push_back(const T &x)
Append an element to the end of the Array.
Logging interface.
Definition Logger.hpp:87
This String class is an abstract data type that represents as NULL terminated string of characters.
Definition String.hpp:67
const char * c_str() const
Definition String.cpp:905
BLOCXX_COMMON_API std::pair< ESecurity, String > security(String const &path, UserId uid)
BLOCXX_COMMON_API String getCurrentWorkingDirectory()
Get the process's current working directory.
String getUserName(uid_t uid, bool &ok)
If the username is invalid, or if getUserName() fails for any other reason, 'success' will be set to ...
Taken from RFC 1321.
EFileStatusReturn getFileStatus(struct stat const &x, uid_t uid, bool is_full_path, const String &path)
GetFileStatus() - just to unify the call of file_ok() for Win and xNix.
BLOCXX_COMMON_API bool isPathAbsolute(String const &path)
Array< std::pair< String, EFileStatusReturn > > path_results_t