about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEelco Dolstra <eelco.dolstra@logicblox.com>2015-02-23T13·41+0100
committerEelco Dolstra <eelco.dolstra@logicblox.com>2015-02-23T14·54+0100
commit15d2d3c34e454fb7795998a3a2d73010dfbdec38 (patch)
tree677224a9270e2f70112f58ab7adf8956c293e8fc
parent47bdc52c1bf7bcec0ea1b685cf4c22b6b93be06d (diff)
Add restricted evaluation mode
If ‘--option restrict-eval true’ is given, the evaluator will throw an
exception if an attempt is made to access any file outside of the Nix
search path. This is primarily intended for Hydra, where we don't want
people doing ‘builtins.readFile ~/.ssh/id_dsa’ or stuff like that.
-rw-r--r--doc/manual/command-ref/conf-file.xml15
-rw-r--r--src/libexpr/eval.cc23
-rw-r--r--src/libexpr/eval.hh6
-rw-r--r--src/libexpr/nixexpr.hh1
-rw-r--r--src/libexpr/parser.y3
-rw-r--r--src/libexpr/primops.cc28
6 files changed, 65 insertions, 11 deletions
diff --git a/doc/manual/command-ref/conf-file.xml b/doc/manual/command-ref/conf-file.xml
index 1728abfd9c59..91aa910a2946 100644
--- a/doc/manual/command-ref/conf-file.xml
+++ b/doc/manual/command-ref/conf-file.xml
@@ -539,6 +539,21 @@ flag, e.g. <literal>--option gc-keep-outputs false</literal>.</para>
   </varlistentry>
 
 
+  <varlistentry xml:id="conf-restrict-eval"><term><literal>restrict-eval</literal></term>
+
+    <listitem>
+
+      <para>If set to <literal>true</literal>, the Nix evaluator will
+      not allow access to any files outside of the Nix search path (as
+      set via the <envar>NIX_PATH</envar> environment variable or the
+      <option>-I</option> option). The default is
+      <literal>false</literal>.</para>
+
+    </listitem>
+
+  </varlistentry>
+
+
 </variablelist>
 
 </para>
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 95b56e84d89a..d82d1d6aafa2 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 f7415fb78dfd..bfaa4081d488 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 121dc58f25f7..ef07d4557fe8 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 d70d29be8ba7..664d6692f51e 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<string, Path>(prefix, path));
+        /* Resolve symlinks in the path to support restricted mode. */
+        searchPath.push_back(std::pair<string, Path>(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 a4efd397ec7a..a4363c678a16 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<string, Path>(prefix, path));
+        searchPath.push_back(std::pair<string, Path>(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);