CVE-2016-6494: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: The client in MongoDB uses world-readable permissions on .dbshell history files, which might allow local users to obtain sensitive information by reading these files.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 5.5
Published: 2016-10-03T18:59Z
Repository: https://github.com/mongodb/mongo
Language: C++
Files changed: jstests/noPassthrough/shell_history.js, src/mongo/shell/linenoise.cpp
Commit date: 2016-09-06 11:45:16 +1000
Commit message: SERVER-25335 avoid group and other permissions when creating .dbshell history file

Diff: diff --git a/jstests/noPassthrough/shell_history.js b/jstests/noPassthrough/shell_history.js
new file mode 100644
index 0000000000..72300a7d8a
--- /dev/null
+++ b/jstests/noPassthrough/shell_history.js
@@ -0,0 +1,103 @@
+// Test that when running the shell for the first time creates the ~/.dbshell file, and it has
+// appropriate permissions (where relevant).
+
+(function() {
+    "use strict";
+
+    // Use dataPath because it includes the trailing "/" or "\".
+    var tmpHome = MongoRunner.dataPath;
+    // Ensure it exists and is a dir (eg. if running without resmoke.py and /data/db doesn't exist).
+    mkdir(tmpHome);
+    removeFile(tmpHome + ".dbshell");
+
+    var args = [];
+    var cmdline = "mongo --nodb";
+    var redirection = "";
+    var env = {};
+    if (_isWindows()) {
+        args.push("cmd.exe");
+        args.push("/c");
+
+        // Input is set to NUL.  The output must also be redirected to NUL, otherwise running the
+        // jstest manually has strange terminal IO behaviour.
+        redirection = "< NUL > NUL";
+
+        // USERPROFILE set to the tmp homedir.
+        // Since NUL is a character device, isatty() will return true, which means that .mongorc.js
+        // will be created in the HOMEDRIVE + HOMEPATH location, so we must set them also.
+        if (tmpHome.match("^[a-zA-Z]:")) {
+            var tmpHomeDrive = tmpHome.substr(0, 2);
+            var tmpHomePath = tmpHome.substr(2);
+        } else {
+            var _pwd = pwd();
+            assert(_pwd.match("^[a-zA-Z]:"), "pwd must include drive");
+            var tmpHomeDrive = _pwd.substr(0, 2);
+            var tmpHomePath = tmpHome;
+        }
+        env = {USERPROFILE: tmpHome, HOMEDRIVE: tmpHomeDrive, HOMEPATH: tmpHomePath};
+
+    } else {
+        args.push("sh");
+        args.push("-c");
+
+        // Use the mongo shell from the current dir, same as resmoke.py does.
+        // Doesn't handle resmoke's --mongo= option.
+        cmdline = "./" + cmdline;
+
+        // Set umask to 0 prior to running the shell.
+        cmdline = "umask 0 ; " + cmdline;
+
+        // stdin is /dev/null.
+        redirection = "< /dev/null";
+
+        // HOME set to the tmp homedir.
+        if (!tmpHome.startsWith("/")) {
+            tmpHome = pwd() + "/" + tmpHome;
+        }
+        env = {HOME: tmpHome};
+    }
+
+    // Add redirection to cmdline, and add cmdline to args.
+    cmdline += " " + redirection;
+    args.push(cmdline);
+    jsTestLog("Running args:\n    " + tojson(args) + "\nwith env:\n    " + tojson(env));
+    var pid = _startMongoProgram({args, env});
+    var rc = waitProgram(pid);
+
+    assert.eq(rc, 0);
+
+    var files = listFiles(tmpHome);
+    jsTestLog(tojson(files));
+
+    var findFile = function(baseName) {
+        for (var i = 0; i < files.length; i++) {
+            if (files[i].baseName === baseName) {
+                return files[i];
+            }
+        }
+        return undefined;
+    };
+
+    var targetFile = ".dbshell";
+    var file = findFile(targetFile);
+
+    assert.neq(typeof(file), "undefined", targetFile + " should exist, but it doesn't");
+    assert.eq(file.isDirectory, false, targetFile + " should not be a directory, but it is");
+    assert.eq(file.size, 0, targetFile + " should be empty, but it isn't");
+
+    if (!_isWindows()) {
+        // On Unix, check that the file has the correct mode (permissions).
+        // The shell has no way to stat a file.
+        // There is no stat utility in POSIX.
+        // `ls -l` is POSIX, so this is the best that we have.
+        // Check for exactly "-rw-------".
+        clearRawMongoProgramOutput();
+        var rc = runProgram("ls", "-l", file.name);
+        assert.eq(rc, 0);
+        var output = rawMongoProgramOutput();
+        var fields = output.split(" ");
+        // First field is the prefix, second field is the `ls -l` permissions.
+        assert.eq(fields[1], "-rw-------", targetFile + " has bad permissions");
+    }
+
+})();
diff --git a/src/mongo/shell/linenoise.cpp b/src/mongo/shell/linenoise.cpp
index f75028c884..8b70214ac5 100644
--- a/src/mongo/shell/linenoise.cpp
+++ b/src/mongo/shell/linenoise.cpp
@@ -2760,21 +2760,31 @@ int linenoiseHistorySetMaxLen(int len) {
 }
 
 /* Save the history in the specified file. On success 0 is returned
  * otherwise -1 is returned. */
 int linenoiseHistorySave(const char* filename) {
-    FILE* fp = fopen(filename, "wt");
+    FILE* fp;
+#if _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
+    int fd = open(filename, O_CREAT, S_IRUSR | S_IWUSR);
+    if (fd == -1) {
+        // report errno somehow?
+        return -1;
+    }
+    fp = fdopen(fd, "wt");
+#else
+    fp = fopen(filename, "wt");
+#endif  // _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
     if (fp == NULL) {
         return -1;
     }
 
     for (int j = 0; j < historyLen; ++j) {
         if (history[j][0] != '\0') {
             fprintf(fp, "%s\n", history[j]);
         }
     }
-    fclose(fp);
+    fclose(fp);  // Also causes fd to be closed.
     return 0;
 }
 
 /* Load the history from the specified file. If the file does not exist
  * zero is returned and no operation is performed.

Vulnerable code:     FILE* fp = fopen(filename, "wt");
    fclose(fp);
Fixed code: // Test that when running the shell for the first time creates the ~/.dbshell file, and it has
// appropriate permissions (where relevant).

(function() {
    "use strict";

    // Use dataPath because it includes the trailing "/" or "\".
    var tmpHome = MongoRunner.dataPath;
    // Ensure it exists and is a dir (eg. if running without resmoke.py and /data/db doesn't exist).
    mkdir(tmpHome);
    removeFile(tmpHome + ".dbshell");

    var args = [];
    var cmdline = "mongo --nodb";
    var redirection = "";
    var env = {};
    if (_isWindows()) {
        args.push("cmd.exe");
        args.push("/c");

        // Input is set to NUL.  The output must also be redirected to NUL, otherwise running the
        // jstest manually has strange terminal IO behaviour.
        redirection = "< NUL > NUL";

        // USERPROFILE set to the tmp homedir.
        // Since NUL is a character device, isatty() will return true, which means that .mongorc.js
        // will be created in the HOMEDRIVE + HOMEPATH location, so we must set them also.
        if (tmpHome.match("^[a-zA-Z]:")) {
            var tmpHomeDrive = tmpHome.substr(0, 2);
            var tmpHomePath = tmpHome.substr(2);
        } else {
            var _pwd = pwd();
            assert(_pwd.match("^[a-zA-Z]:"), "pwd must include drive");
            var tmpHomeDrive = _pwd.substr(0, 2);
            var tmpHomePath = tmpHome;
        }
        env = {USERPROFILE: tmpHome, HOMEDRIVE: tmpHomeDrive, HOMEPATH: tmpHomePath};

    } else {
        args.push("sh");
        args.push("-c");

        // Use the mongo shell from the current dir, same as resmoke.py does.
        // Doesn't handle resmoke's --mongo= option.
        cmdline = "./" + cmdline;

        // Set umask to 0 prior to running the shell.
        cmdline = "umask 0 ; " + cmdline;

        // stdin is /dev/null.
        redirection = "< /dev/null";

        // HOME set to the tmp homedir.
        if (!tmpHome.startsWith("/")) {
            tmpHome = pwd() + "/" + tmpHome;
        }
        env = {HOME: tmpHome};
    }

    // Add redirection to cmdline, and add cmdline to args.
    cmdline += " " + redirection;
    args.push(cmdline);
    jsTestLog("Running args:\n    " + tojson(args) + "\nwith env:\n    " + tojson(env));
    var pid = _startMongoProgram({args, env});
    var rc = waitProgram(pid);

    assert.eq(rc, 0);

    var files = listFiles(tmpHome);
    jsTestLog(tojson(files));

    var findFile = function(baseName) {
        for (var i = 0; i < files.length; i++) {
            if (files[i].baseName === baseName) {
                return files[i];
            }
        }
        return undefined;
    };

    var targetFile = ".dbshell";
    var file = findFile(targetFile);

    assert.neq(typeof(file), "undefined", targetFile + " should exist, but it doesn't");
    assert.eq(file.isDirectory, false, targetFile + " should not be a directory, but it is");
    assert.eq(file.size, 0, targetFile + " should be empty, but it isn't");

    if (!_isWindows()) {
        // On Unix, check that the file has the correct mode (permissions).
        // The shell has no way to stat a file.
        // There is no stat utility in POSIX.
        // `ls -l` is POSIX, so this is the best that we have.
        // Check for exactly "-rw-------".
        clearRawMongoProgramOutput();
        var rc = runProgram("ls", "-l", file.name);
        assert.eq(rc, 0);
        var output = rawMongoProgramOutput();
        var fields = output.split(" ");
        // First field is the prefix, second field is the `ls -l` permissions.
        assert.eq(fields[1], "-rw-------", targetFile + " has bad permissions");
    }

})();
    FILE* fp;
#if _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
    int fd = open(filename, O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        // report errno somehow?
        return -1;
    }
    fp = fdopen(fd, "wt");
#else
    fp = fopen(filename, "wt");
#endif  // _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
    fclose(fp);  // Also causes fd to be closed.