forked from lix-project/lix
Fix symlink leak in restricted eval mode
In EvalState::checkSourcePath, the path is checked against the list of allowed paths first and later it's checked again *after* resolving symlinks. The resolving of the symlinks is done via canonPath, which also strips out "../" and "./". However after the canonicalisation the error message pointing out that the path is not allowed prints the symlink target in the error message. Even if we'd suppress the message, symlink targets could still be leaked if the symlink target doesn't exist (in this case the error is thrown in canonPath). So instead, we now do canonPath() without symlink resolving first before even checking against the list of allowed paths and then later do the symlink resolving and checking the allowed paths again. The first call to canonPath() should get rid of all the "../" and "./", so in theory the only way to leak a symlink if the attacker is able to put a symlink in one of the paths allowed by restricted evaluation mode. For the latter I don't think this is part of the threat model, because if the attacker can write to that path, the attack vector is even larger. Signed-off-by: aszlig <aszlig@nix.build>
This commit is contained in:
parent
aa64e95bc8
commit
43e28a1b75
|
@ -349,19 +349,25 @@ Path EvalState::checkSourcePath(const Path & path_)
|
||||||
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
|
|
||||||
|
/* First canonicalize the path without symlinks, so we make sure an
|
||||||
|
* attacker can't append ../../... to a path that would be in allowedPaths
|
||||||
|
* and thus leak symlink targets.
|
||||||
|
*/
|
||||||
|
Path abspath = canonPath(path_);
|
||||||
|
|
||||||
for (auto & i : *allowedPaths) {
|
for (auto & i : *allowedPaths) {
|
||||||
if (isDirOrInDir(path_, i)) {
|
if (isDirOrInDir(abspath, i)) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found)
|
if (!found)
|
||||||
throw RestrictedPathError("access to path '%1%' is forbidden in restricted mode", path_);
|
throw RestrictedPathError("access to path '%1%' is forbidden in restricted mode", abspath);
|
||||||
|
|
||||||
/* Resolve symlinks. */
|
/* Resolve symlinks. */
|
||||||
debug(format("checking access to '%s'") % path_);
|
debug(format("checking access to '%s'") % abspath);
|
||||||
Path path = canonPath(path_, true);
|
Path path = canonPath(abspath, true);
|
||||||
|
|
||||||
for (auto & i : *allowedPaths) {
|
for (auto & i : *allowedPaths) {
|
||||||
if (isDirOrInDir(path, i)) {
|
if (isDirOrInDir(path, i)) {
|
||||||
|
|
|
@ -38,3 +38,14 @@ ln -sfn $(pwd)/restricted.nix $TEST_ROOT/restricted.nix
|
||||||
nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT -I .
|
nix-instantiate --eval --restrict-eval $TEST_ROOT/restricted.nix -I $TEST_ROOT -I .
|
||||||
|
|
||||||
[[ $(nix eval --raw --restrict-eval -I . '(builtins.readFile "${import ./simple.nix}/hello")') == 'Hello World!' ]]
|
[[ $(nix eval --raw --restrict-eval -I . '(builtins.readFile "${import ./simple.nix}/hello")') == 'Hello World!' ]]
|
||||||
|
|
||||||
|
# Check whether we can leak symlink information through directory traversal.
|
||||||
|
traverseDir="$(pwd)/restricted-traverse-me"
|
||||||
|
ln -sfn "$(pwd)/restricted-secret" "$(pwd)/restricted-innocent"
|
||||||
|
mkdir -p "$traverseDir"
|
||||||
|
goUp="..$(echo "$traverseDir" | sed -e 's,[^/]\+,..,g')"
|
||||||
|
output="$(nix eval --raw --restrict-eval -I "$traverseDir" \
|
||||||
|
"(builtins.readFile \"$traverseDir/$goUp$(pwd)/restricted-innocent\")" \
|
||||||
|
2>&1 || :)"
|
||||||
|
echo "$output" | grep "is forbidden"
|
||||||
|
! echo "$output" | grep -F restricted-secret
|
||||||
|
|
Loading…
Reference in a new issue