diff --git a/doc/manual/command-ref/conf-file.xml b/doc/manual/command-ref/conf-file.xml index 1728abfd9..91aa910a2 100644 --- a/doc/manual/command-ref/conf-file.xml +++ b/doc/manual/command-ref/conf-file.xml @@ -539,6 +539,21 @@ flag, e.g. --option gc-keep-outputs false. + restrict-eval + + + + If set to true, the Nix evaluator will + not allow access to any files outside of the Nix search path (as + set via the NIX_PATH environment variable or the + option). The default is + false. + + + + + + diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 95b56e84d..d82d1d6aa 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -195,6 +195,8 @@ EvalState::EvalState(const Strings & _searchPath) nrListConcats = nrPrimOpCalls = nrFunctionCalls = 0; countCalls = getEnv("NIX_COUNT_CALLS", "0") != "0"; + restricted = settings.get("restrict-eval", false); + #if HAVE_BOEHMGC static bool gcInitialised = false; if (!gcInitialised) { @@ -250,6 +252,21 @@ EvalState::~EvalState() } +Path EvalState::checkSourcePath(const Path & path_) +{ + if (!restricted) return path_; + + /* Resolve symlinks. */ + Path path = canonPath(path_, true); + + for (auto & i : searchPath) + if (path == i.second || isInDir(path, i.second)) + return path; + + throw RestrictedPathError(format("access to path ‘%1%’ is forbidden in restricted mode") % path_); +} + + void EvalState::addConstant(const string & name, Value & v) { Value * v2 = allocValue(); @@ -555,7 +572,7 @@ void EvalState::evalFile(const Path & path, Value & v) } startNest(nest, lvlTalkative, format("evaluating file ‘%1%’") % path2); - Expr * e = parseExprFromFile(path2); + Expr * e = parseExprFromFile(checkSourcePath(path2)); try { eval(e, v); } catch (Error & e) { @@ -1358,8 +1375,8 @@ string EvalState::copyPathToStore(PathSet & context, const Path & path) dstPath = srcToStore[path]; else { dstPath = settings.readOnlyMode - ? computeStorePathForPath(path).first - : store->addToStore(path, true, htSHA256, defaultPathFilter, repair); + ? computeStorePathForPath(checkSourcePath(path)).first + : store->addToStore(checkSourcePath(path), true, htSHA256, defaultPathFilter, repair); srcToStore[path] = dstPath; printMsg(lvlChatty, format("copied source ‘%1%’ -> ‘%2%’") % path % dstPath); diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index f7415fb78..bfaa4081d 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -135,6 +135,10 @@ public: already exist there. */ bool repair; + /* If set, don't allow access to files outside of the Nix search + path or to environment variables. */ + bool restricted; + private: SrcToStore srcToStore; @@ -155,6 +159,8 @@ public: void addToSearchPath(const string & s, bool warn = false); + Path checkSourcePath(const Path & path); + /* Parse a Nix expression from the specified file. */ Expr * parseExprFromFile(const Path & path); Expr * parseExprFromFile(const Path & path, StaticEnv & staticEnv); diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index 121dc58f2..ef07d4557 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -16,6 +16,7 @@ MakeError(ThrownError, AssertionError) MakeError(Abort, EvalError) MakeError(TypeError, EvalError) MakeError(UndefinedVarError, Error) +MakeError(RestrictedPathError, Error) /* Position objects. */ diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index d70d29be8..664d6692f 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -614,7 +614,8 @@ void EvalState::addToSearchPath(const string & s, bool warn) path = absPath(path); if (pathExists(path)) { debug(format("adding path ‘%1%’ to the search path") % path); - searchPath.push_back(std::pair(prefix, path)); + /* Resolve symlinks in the path to support restricted mode. */ + searchPath.push_back(std::pair(prefix, canonPath(path, true))); } else if (warn) printMsg(lvlError, format("warning: Nix search path entry ‘%1%’ does not exist, ignoring") % path); } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index a4efd397e..a4363c678 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -80,6 +80,8 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args % path % e.path % pos); } + path = state.checkSourcePath(path); + if (isStorePath(path) && store->isValidPath(path) && isDerivation(path)) { Derivation drv = readDerivation(path); Value & w = *state.allocValue(); @@ -133,7 +135,7 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args /* !!! Should we pass the Pos or the file name too? */ extern "C" typedef void (*ValueInitializer)(EvalState & state, Value & v); -/* Load a ValueInitializer from a dso and return whatever it initializes */ +/* Load a ValueInitializer from a DSO and return whatever it initializes */ static void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value & v) { PathSet context; @@ -146,6 +148,8 @@ static void prim_importNative(EvalState & state, const Pos & pos, Value * * args % path % e.path % pos); } + path = state.checkSourcePath(path); + string sym = state.forceStringNoCtx(*args[1], pos); void *handle = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL); @@ -380,7 +384,7 @@ static void prim_tryEval(EvalState & state, const Pos & pos, Value * * args, Val static void prim_getEnv(EvalState & state, const Pos & pos, Value * * args, Value & v) { string name = state.forceStringNoCtx(*args[0], pos); - mkString(v, getEnv(name)); + mkString(v, state.restricted ? "" : getEnv(name)); } @@ -680,7 +684,7 @@ static void prim_toPath(EvalState & state, const Pos & pos, Value * * args, Valu static void prim_storePath(EvalState & state, const Pos & pos, Value * * args, Value & v) { PathSet context; - Path path = state.coerceToPath(pos, *args[0], context); + Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context)); /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink directly in the store. The latter condition is necessary so e.g. nix-push does the right thing. */ @@ -701,7 +705,15 @@ static void prim_pathExists(EvalState & state, const Pos & pos, Value * * args, Path path = state.coerceToPath(pos, *args[0], context); if (!context.empty()) throw EvalError(format("string ‘%1%’ cannot refer to other paths, at %2%") % path % pos); - mkBool(v, pathExists(path)); + try { + mkBool(v, pathExists(state.checkSourcePath(path))); + } catch (SysError & e) { + /* Don't give away info from errors while canonicalising + ‘path’ in restricted mode. */ + mkBool(v, false); + } catch (RestrictedPathError & e) { + mkBool(v, false); + } } @@ -736,7 +748,7 @@ static void prim_readFile(EvalState & state, const Pos & pos, Value * * args, Va throw EvalError(format("cannot read ‘%1%’, since path ‘%2%’ is not valid, at %3%") % path % e.path % pos); } - mkString(v, readFile(path).c_str()); + mkString(v, readFile(state.checkSourcePath(path)).c_str()); } @@ -763,7 +775,7 @@ static void prim_findFile(EvalState & state, const Pos & pos, Value * * args, Va throw EvalError(format("attribute ‘path’ missing, at %1%") % pos); string path = state.coerceToPath(pos, *i->value, context); - searchPath.push_back(std::pair(prefix, path)); + searchPath.push_back(std::pair(prefix, state.checkSourcePath(path))); } string path = state.forceStringNoCtx(*args[1], pos); @@ -790,7 +802,7 @@ static void prim_readDir(EvalState & state, const Pos & pos, Value * * args, Val % path % e.path % pos); } - DirEntries entries = readDirectory(path); + DirEntries entries = readDirectory(state.checkSourcePath(path)); state.mkAttrs(v, entries.size()); for (auto & ent : entries) { @@ -927,6 +939,8 @@ static void prim_filterSource(EvalState & state, const Pos & pos, Value * * args FilterFromExpr filter(state, *args[0]); + path = state.checkSourcePath(path); + Path dstPath = settings.readOnlyMode ? computeStorePathForPath(path, true, htSHA256, filter).first : store->addToStore(path, true, htSHA256, filter, state.repair);