// ----------------------------------------------------------------------
// File: SecurityChecker.cc
// Author: Georgios Bitzes - CERN
// ----------------------------------------------------------------------
/************************************************************************
* EOS - the CERN Disk Storage System *
* Copyright (C) 2011 CERN/Switzerland *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see .*
************************************************************************/
#include "SecurityChecker.hh"
#include "ScopedFsUidSetter.hh"
#include "FileDescriptor.hh"
#include "Utils.hh"
#include "common/Logging.hh"
#include
#include
#include
//------------------------------------------------------------------------------
// Portability helper: Extract timespec from stat struct
//------------------------------------------------------------------------------
struct timespec extractTimespec(const struct stat& st)
{
#ifdef __APPLE__
return st.st_mtimespec;
#else
return st.st_mtim;
#endif
}
//------------------------------------------------------------------------------
// Constructor
//------------------------------------------------------------------------------
SecurityChecker::SecurityChecker(bool ij) : ignoreJails(ij) {}
//------------------------------------------------------------------------------
// Inject the given fake data. Once an injection is active, _all_ returned
// data is faked.
//------------------------------------------------------------------------------
void SecurityChecker::inject(const JailIdentifier& jail,
const std::string& path, uid_t uid, mode_t mode, struct timespec mtime)
{
std::lock_guard lock(mtx);
useInjectedData = true;
injections[path] = InjectedData(uid, mode, mtime);
}
//------------------------------------------------------------------------------
// Same as lookup, but only serve simulated data.
//------------------------------------------------------------------------------
SecurityChecker::Info SecurityChecker::lookupInjected(
const JailIdentifier& jail, const std::string& path, uid_t uid)
{
std::lock_guard lock(mtx);
auto it = injections.find(path);
if (it == injections.end()) return {};
if (!checkPermissions(it->second.uid, it->second.mode, uid)) {
return Info(CredentialState::kBadPermissions, {0, 0});
}
return Info(CredentialState::kOk, it->second.mtime);
}
//------------------------------------------------------------------------------
// We have a file with the given uid and mode, and we're "expectedUid".
// Should we be able to read it? Enforce strict permissions on mode, as it's
// a credential file - only _we_ should be able to read it and no-one else.
//------------------------------------------------------------------------------
bool SecurityChecker::checkPermissions(uid_t uid, mode_t mode,
uid_t expectedUid)
{
if (uid != expectedUid) {
return false;
}
if ((mode & 0077) != 0) {
// No access to other users/groups
return false;
}
if ((mode & 0400) == 0) {
// Read should be allowed for the user
return false;
}
return true;
}
//------------------------------------------------------------------------------
// Lookup given path in the context of our local jail.
//------------------------------------------------------------------------------
SecurityChecker::Info SecurityChecker::lookupLocalJail(const std::string& path,
uid_t uid)
{
std::string resolvedPath;
// is "path" a symlink?
char buffer[1024];
const ssize_t retsize = readlink(path.c_str(), buffer, 1023);
if (retsize != -1) {
resolvedPath = std::string(buffer, retsize);
} else {
resolvedPath = path;
}
struct stat filestat;
if (::stat(resolvedPath.c_str(), &filestat) != 0) {
// cannot stat
return {};
}
if (!checkPermissions(filestat.st_uid, filestat.st_mode, uid)) {
eos_static_alert("Uid %d is asking to use credentials '%s', but file "
"belongs to uid %d! Refusing.", uid, path.c_str(), filestat.st_uid);
return Info(CredentialState::kBadPermissions, {0, 0});
}
return Info(CredentialState::kOk, extractTimespec(filestat));
}
//------------------------------------------------------------------------------
// Things have gotten serious - interpret given path in the context of a
// different jail, and return entire contents.
//------------------------------------------------------------------------------
SecurityChecker::Info SecurityChecker::lookupNonLocalJail(
const JailInformation& jail, const std::string& path, uid_t uid, gid_t gid)
{
//----------------------------------------------------------------------------
// First, let's open the jail as root.
//----------------------------------------------------------------------------
std::string jailPath = SSTR("/proc/" << jail.pid << "/root");
FileDescriptor jailfd(open(jailPath.c_str(), O_DIRECTORY | O_RDONLY));
if (!jailfd.ok()) {
eos_static_alert("Opening jail '%s' failed", jailPath.c_str());
return Info(CredentialState::kCannotStat, {0, 0});
}
//----------------------------------------------------------------------------
// Reset my fsuid, fsgid to user-provided ones.
//----------------------------------------------------------------------------
#ifdef __linux__
ScopedFsUidSetter uidSetter(uid, gid);
if (!uidSetter.IsOk()) {
eos_static_alert("Setting uid,gid to %d,%d failed", uid, gid);
return Info(CredentialState::kCannotStat, {0, 0});
}
#endif
//----------------------------------------------------------------------------
// User-space lookup of path - this could be avoided if the linux kernel
// supported openat with AT_THIS_ROOT ...
//----------------------------------------------------------------------------
if (!eos::common::startsWith(path, "/")) {
//--------------------------------------------------------------------------
// User is attempting to open a relative path ?! No.
//--------------------------------------------------------------------------
eos_static_alert("Forbidden relative path '%s' ", path.c_str());
return Info(CredentialState::kCannotStat, {0, 0} );
}
FileDescriptor current = std::move(jailfd);
auto splitPath = eos::common::SplitPath(path);
for (size_t i=0; i< splitPath.size() -1; i++) {
//--------------------------------------------------------------------------
// ".." in path? Disallow for now.
//--------------------------------------------------------------------------
if(splitPath[i] == "..") {
eos_static_alert(".. in sub-path");
return Info(CredentialState::kCannotStat, {0, 0} );
}
FileDescriptor next(openat(current.getFD(), splitPath[i].c_str(),
O_DIRECTORY | O_NOFOLLOW | O_RDONLY));
if(!next.ok()) {
eos_static_alert("Failed to openat next child");
return Info(CredentialState::kCannotStat, {0, 0} );
}
current = std::move(next);
}
//----------------------------------------------------------------------------
// We survived, up to the last chunk. Now try to read file contents.
//----------------------------------------------------------------------------
FileDescriptor fileFd(openat(current.getFD(), splitPath.back().c_str(),
O_NOFOLLOW | O_RDONLY));
if(!fileFd.ok()) {
eos_static_alert("Failed to openat file");
return Info(CredentialState::kCannotStat, {0, 0} );
}
//----------------------------------------------------------------------------
// First stat the fd, make sure file permissions are OK.
//----------------------------------------------------------------------------
struct stat filestat;
if (::fstat(fileFd.getFD(), &filestat) != 0) {
eos_static_alert("failed to stat by fd");
return Info(CredentialState::kCannotStat, {0, 0} );
}
if (!checkPermissions(filestat.st_uid, filestat.st_mode, uid)) {
return Info(CredentialState::kBadPermissions, {0, 0});
}
//----------------------------------------------------------------------------
// All is good, try to read contents.
//----------------------------------------------------------------------------
std::string contents;
if(!readFile(fileFd.getFD(), contents)) {
eos_static_alert("failed to read file");
return Info::CannotStat();
}
//----------------------------------------------------------------------------
// We have the contents, return.
//----------------------------------------------------------------------------
return Info::WithContents(extractTimespec(filestat), contents);
}
//------------------------------------------------------------------------------
// Lookup given path.
//------------------------------------------------------------------------------
SecurityChecker::Info SecurityChecker::lookup(const JailInformation& jail,
const std::string& path, uid_t uid, gid_t gid)
{
//----------------------------------------------------------------------------
// Simulation?
//----------------------------------------------------------------------------
if (useInjectedData) {
return lookupInjected(jail.id, path, uid);
}
//----------------------------------------------------------------------------
// Nope, real thing.
//----------------------------------------------------------------------------
if (path.empty()) {
return {};
}
//----------------------------------------------------------------------------
// Is the request towards our local jail? If so, use fast path, no need to
// go through heavyweight remote-jail lookup.
//
// Also, if ignoreJails is set to true we ignore containerization completely,
// and treat all paths relative to the host.
//----------------------------------------------------------------------------
if (jail.sameJailAsThisPid || ignoreJails) {
return lookupLocalJail(path, uid);
}
return lookupNonLocalJail(jail, path, uid, gid);
}