-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[clang][analyzer] Add checker 'security.SetgidSetuidOrder' #91445
Changes from 7 commits
d839faf
996c22c
50d996a
49b3fe9
be329ce
ae31b7b
5f97049
1493978
96b340a
704cba8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1179,6 +1179,47 @@ security.insecureAPI.DeprecatedOrUnsafeBufferHandling (C) | |
strncpy(buf, "a", 1); // warn | ||
} | ||
|
||
security.SetgidSetuidOrder (C) | ||
"""""""""""""""""""""""""""""" | ||
When dropping user-level and group-level privileges in a program by using | ||
``setuid`` and ``setgid`` calls, it is important to reset the group-level | ||
privileges (with ``setgid``) first. Function ``setgid`` will likely fail if | ||
the superuser privileges are already dropped. | ||
|
||
The checker checks for sequences of ``setuid(getuid())`` and | ||
``setgid(getgid())`` calls (in this order). If such a sequence is found and | ||
there is no other privilege-changing function call (``seteuid``, ``setreuid``, | ||
``setresuid`` and the GID versions of these) in between, a warning is | ||
generated. The checker finds only exactly ``setuid(getuid())`` calls (and the | ||
GID versions), not for example if the result of ``getuid()`` is stored in a | ||
variable. | ||
|
||
.. code-block:: c | ||
|
||
void test1() { | ||
// ... | ||
// end of section with elevated privileges | ||
// reset privileges (user and group) to normal user | ||
if (setuid(getuid()) != 0) { | ||
handle_error(); | ||
return; | ||
} | ||
if (setgid(getgid()) != 0) { // warning: A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail | ||
handle_error(); | ||
return; | ||
} | ||
// user-ID and group-ID are reset to normal user now | ||
// ... | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the docs should explicitly mention how to fix/suppress this issue. And this example looks suspiciously similar to the one present in the CERT docs. |
||
In the code above the problem is that ``setuid(getuid())`` removes superuser | ||
privileges before ``setgid(getgid())`` is called. To fix the problem the | ||
``setgid(getgid())`` should be called first. Further attention is needed to | ||
avoid code like ``setgid(getuid())`` (this checker does not detect bugs like | ||
this). | ||
|
||
This check corresponds to SEI CERT Rule `POS36-C <https://wiki.sei.cmu.edu/confluence/display/c/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges>`_. | ||
|
||
.. _unix-checkers: | ||
|
||
unix | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
//===-- SetgidSetuidOrderChecker.cpp - check privilege revocation calls ---===// | ||
// | ||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
// See https://llvm.org/LICENSE.txt for license information. | ||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
// | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This file defines a checker to detect possible reversed order of privilege | ||
// revocations when 'setgid' and 'setuid' is used. | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" | ||
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" | ||
#include "clang/StaticAnalyzer/Core/Checker.h" | ||
#include "clang/StaticAnalyzer/Core/CheckerManager.h" | ||
#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h" | ||
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" | ||
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" | ||
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" | ||
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h" | ||
|
||
using namespace clang; | ||
using namespace ento; | ||
|
||
namespace { | ||
|
||
enum SetPrivilegeFunctionKind { Irrelevant, Setuid, Setgid }; | ||
|
||
class SetgidSetuidOrderChecker : public Checker<check::PostCall, eval::Assume> { | ||
const BugType BT{this, "Possible wrong order of privilege revocation"}; | ||
|
||
const CallDescription SetuidDesc{CDM::CLibrary, {"setuid"}, 1}; | ||
const CallDescription SetgidDesc{CDM::CLibrary, {"setgid"}, 1}; | ||
|
||
const CallDescription GetuidDesc{CDM::CLibrary, {"getuid"}, 0}; | ||
const CallDescription GetgidDesc{CDM::CLibrary, {"getgid"}, 0}; | ||
|
||
const CallDescriptionSet OtherSetPrivilegeDesc{ | ||
{CDM::CLibrary, {"seteuid"}, 1}, {CDM::CLibrary, {"setegid"}, 1}, | ||
{CDM::CLibrary, {"setreuid"}, 2}, {CDM::CLibrary, {"setregid"}, 2}, | ||
{CDM::CLibrary, {"setresuid"}, 3}, {CDM::CLibrary, {"setresgid"}, 3}}; | ||
|
||
public: | ||
void checkPostCall(const CallEvent &Call, CheckerContext &C) const; | ||
ProgramStateRef evalAssume(ProgramStateRef State, SVal Cond, | ||
bool Assumption) const; | ||
const BugType *getBT() const { return &BT; } | ||
steakhal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private: | ||
void processSetuid(ProgramStateRef State, const CallEvent &Call, | ||
CheckerContext &C) const; | ||
void processSetgid(ProgramStateRef State, const CallEvent &Call, | ||
CheckerContext &C) const; | ||
void processOther(ProgramStateRef State, const CallEvent &Call, | ||
CheckerContext &C) const; | ||
/// Check if a function like \c getuid or \c getgid is called directly from | ||
/// the first argument of function called from \a Call. | ||
bool isFunctionCalledInArg(const CallDescription &Desc, | ||
const CallEvent &Call) const; | ||
void emitReport(ProgramStateRef State, CheckerContext &C) const; | ||
}; | ||
|
||
} // end anonymous namespace | ||
|
||
/// Store if there was a call to 'setuid(getuid())' or 'setgid(getgid())' not | ||
/// followed by other different privilege-change functions. | ||
/// If the value \c Setuid is stored and a 'setgid(getgid())' call is found we | ||
/// have found the bug to be reported. Value \c Setgid is used too to prevent | ||
/// warnings at a setgid-setuid-setgid sequence. | ||
REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetPrivilegeCall, SetPrivilegeFunctionKind) | ||
/// Store the symbol value of the last 'setuid(getuid())' call. This is used to | ||
/// detect if the result is compared to -1 and avoid warnings on that branch | ||
/// (which is the failure branch of the call), and for identification of note | ||
/// tags. | ||
REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetuidCallSVal, SymbolRef) | ||
|
||
void SetgidSetuidOrderChecker::checkPostCall(const CallEvent &Call, | ||
CheckerContext &C) const { | ||
ProgramStateRef State = C.getState(); | ||
if (SetuidDesc.matches(Call)) { | ||
processSetuid(State, Call, C); | ||
} else if (SetgidDesc.matches(Call)) { | ||
processSetgid(State, Call, C); | ||
} else if (OtherSetPrivilegeDesc.contains(Call)) { | ||
processOther(State, Call, C); | ||
} | ||
} | ||
|
||
ProgramStateRef SetgidSetuidOrderChecker::evalAssume(ProgramStateRef State, | ||
SVal Cond, | ||
bool Assumption) const { | ||
Comment on lines
+90
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is very strange that this @haoNoQ @Xazax-hun @steakhal Do you happen to have architectural insight about this situation? How did we end up with a callback where the arguments are mostly unused? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case it would be possible to examine what is inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @balazske Thanks for the explanation. This question is still somewhat relevant as an architectural curiosity, but there is no need to change this part of this commit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that it has limited usability. We eagerly fold expressions (see Now that I grepped for users, they are usually interested in if a symbol gets perfectly constrained; after which they do two things: cleanup some map and/or set some traits. I think a callback for perfect constraints makes sense, but not for arbitrary assumptions. |
||
SValBuilder &SVB = State->getStateManager().getSValBuilder(); | ||
SymbolRef LastSetuidSym = State->get<LastSetuidCallSVal>(); | ||
if (!LastSetuidSym) | ||
return State; | ||
|
||
// Check if the most recent call to 'setuid(getuid())' is assumed to be != 0. | ||
// It should be only -1 at failure, but we want to accept a "!= 0" check too. | ||
// (But now an invalid failure check like "!= 1" will be recognized as correct | ||
// too. The "invalid failure check" is a different bug that is not the scope | ||
// of this checker.) | ||
auto FailComparison = | ||
SVB.evalBinOpNN(State, BO_NE, nonloc::SymbolVal(LastSetuidSym), | ||
SVB.makeIntVal(0, /*isUnsigned=*/false), | ||
SVB.getConditionType()) | ||
.getAs<DefinedOrUnknownSVal>(); | ||
if (!FailComparison) | ||
return State; | ||
if (auto IsFailBranch = State->assume(*FailComparison); | ||
IsFailBranch.first && !IsFailBranch.second) { | ||
// This is the 'setuid(getuid())' != 0 case. | ||
// On this branch we do not want to emit warning. | ||
State = State->set<LastSetPrivilegeCall>(Irrelevant); | ||
State = State->set<LastSetuidCallSVal>(SymbolRef{}); | ||
} | ||
return State; | ||
} | ||
|
||
void SetgidSetuidOrderChecker::processSetuid(ProgramStateRef State, | ||
const CallEvent &Call, | ||
CheckerContext &C) const { | ||
bool IsSetuidWithGetuid = isFunctionCalledInArg(GetuidDesc, Call); | ||
if (State->get<LastSetPrivilegeCall>() != Setgid && IsSetuidWithGetuid) { | ||
SymbolRef RetSym = Call.getReturnValue().getAsSymbol(); | ||
State = State->set<LastSetPrivilegeCall>(Setuid); | ||
State = State->set<LastSetuidCallSVal>(RetSym); | ||
const NoteTag *Note = C.getNoteTag([this, | ||
RetSym](PathSensitiveBugReport &BR) { | ||
if (!BR.isInteresting(RetSym) || &BR.getBugType() != this->getBT()) | ||
steakhal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ""; | ||
return "Call to 'setuid' found here that removes superuser privileges"; | ||
}); | ||
C.addTransition(State, Note); | ||
return; | ||
} | ||
State = State->set<LastSetPrivilegeCall>(Irrelevant); | ||
State = State->set<LastSetuidCallSVal>(SymbolRef{}); | ||
C.addTransition(State); | ||
} | ||
|
||
void SetgidSetuidOrderChecker::processSetgid(ProgramStateRef State, | ||
const CallEvent &Call, | ||
CheckerContext &C) const { | ||
bool IsSetgidWithGetgid = isFunctionCalledInArg(GetgidDesc, Call); | ||
if (State->get<LastSetPrivilegeCall>() == Setuid) { | ||
if (IsSetgidWithGetgid) { | ||
State = State->set<LastSetPrivilegeCall>(Irrelevant); | ||
emitReport(State, C); | ||
return; | ||
} | ||
State = State->set<LastSetPrivilegeCall>(Irrelevant); | ||
} else { | ||
State = State->set<LastSetPrivilegeCall>(IsSetgidWithGetgid ? Setgid | ||
: Irrelevant); | ||
} | ||
State = State->set<LastSetuidCallSVal>(SymbolRef{}); | ||
C.addTransition(State); | ||
} | ||
|
||
void SetgidSetuidOrderChecker::processOther(ProgramStateRef State, | ||
const CallEvent &Call, | ||
CheckerContext &C) const { | ||
State = State->set<LastSetuidCallSVal>(SymbolRef{}); | ||
State = State->set<LastSetPrivilegeCall>(Irrelevant); | ||
C.addTransition(State); | ||
} | ||
|
||
bool SetgidSetuidOrderChecker::isFunctionCalledInArg( | ||
const CallDescription &Desc, const CallEvent &Call) const { | ||
if (const auto *CallInArg0 = | ||
dyn_cast<CallExpr>(Call.getArgExpr(0)->IgnoreParenImpCasts())) | ||
return Desc.matchesAsWritten(*CallInArg0); | ||
return false; | ||
} | ||
|
||
void SetgidSetuidOrderChecker::emitReport(ProgramStateRef State, | ||
CheckerContext &C) const { | ||
if (ExplodedNode *N = C.generateNonFatalErrorNode(State)) { | ||
llvm::StringLiteral Msg = | ||
"A 'setgid(getgid())' call following a 'setuid(getuid())' " | ||
"call is likely to fail; probably the order of these " | ||
"statements is wrong"; | ||
auto Report = std::make_unique<PathSensitiveBugReport>(BT, Msg, N); | ||
Report->markInteresting(State->get<LastSetuidCallSVal>()); | ||
C.emitReport(std::move(Report)); | ||
} | ||
} | ||
|
||
void ento::registerSetgidSetuidOrderChecker(CheckerManager &mgr) { | ||
mgr.registerChecker<SetgidSetuidOrderChecker>(); | ||
} | ||
|
||
bool ento::shouldRegisterSetgidSetuidOrderChecker(const CheckerManager &mgr) { | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Give that the
alpha.unix.chroot
checker does something similar, I wonder if this checker should share the same parent package with that one to aid discoverability.WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move the new checker into
unix
, or move the chroot checker intosecurity
(oralpha.security
)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a preference, I was just asking.
I just felt this is a natural question, and you seem to care about alpha checkers and documentation/discoverability, and this seemed like a subject to discuss. @NagyDonat WDYT?