formatting support for `Subprocess::shellify`
authorDominik Gabi <dominik@fb.com>
Thu, 15 Sep 2016 14:57:14 +0000 (07:57 -0700)
committerFacebook Github Bot 8 <facebook-github-bot-8-bot@fb.com>
Thu, 15 Sep 2016 15:08:45 +0000 (08:08 -0700)
Summary:
Adding formatting support with automatic escaping of parameters. Will
subsequentyl try and make it harder for people to use non-statically determined
format strings for this.

Reviewed By: simpkins

Differential Revision: D3866174

fbshipit-source-id: 80bb5886386692643876116fbf338ca0d6986e6a

folly/Makefile.am
folly/Shell.h [new file with mode: 0644]
folly/Subprocess.cpp
folly/Subprocess.h
folly/test/ShellTest.cpp [new file with mode: 0644]
folly/test/SubprocessTest.cpp

index f9c059bbd3df12fd04531813e9b0901d1c42167d..5a5f66abc694e7cdfe7aa5bd4b87ed0fa9906ea6 100644 (file)
@@ -313,6 +313,7 @@ nobase_follyinclude_HEADERS = \
        RWSpinLock.h \
        ScopeGuard.h \
        SharedMutex.h \
+       Shell.h \
        Singleton.h \
        Singleton-inl.h \
        SingletonThreadLocal.h \
diff --git a/folly/Shell.h b/folly/Shell.h
new file mode 100644 (file)
index 0000000..56df3d2
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `Shell` provides a collection of functions to use with `Subprocess` that make
+ * it easier to safely run processes in a unix shell.
+ *
+ * Note: use this rarely and carefully. By default you should use `Subprocess`
+ * with a vector of arguments.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <folly/Format.h>
+#include <folly/Range.h>
+
+namespace folly {
+
+/**
+ * Quotes an argument to make it suitable for use as shell command arguments.
+ */
+std::string shellQuote(StringPiece argument) {
+  std::string quoted = "'";
+  for (auto c : argument) {
+    if (c == '\'') {
+      quoted += "'\\''";
+    } else {
+      quoted += c;
+    }
+  }
+  return quoted + "'";
+}
+
+/**
+  * Create argument array for `Subprocess()` for a process running in a
+  * shell.
+  *
+  * The shell to use is taken from the environment variable $SHELL,
+  * or /bin/sh if $SHELL is unset.
+  *
+  * The format string should always be a string literal to protect against
+  * shell injections. Arguments will automatically be escaped with `'`.
+  *
+  * TODO(dominik): find a way to ensure statically determined format strings.
+  */
+template <typename... Arguments>
+std::vector<std::string> shellify(
+    const StringPiece format,
+    Arguments&&... arguments) {
+  const char* shell = getenv("SHELL");
+  if (!shell) {
+    shell = "/bin/sh";
+  }
+  auto command = sformat(
+      format,
+      shellQuote(to<std::string>(std::forward<Arguments>(arguments)))...);
+  return {shell, "-c", command};
+}
+
+} // folly
index d6883392c680054b54f7e93f101bce1d570a831d..912b54f2b25bf5cfb1aed0baa61b993217e51baf 100644 (file)
@@ -37,6 +37,7 @@
 #include <folly/Conv.h>
 #include <folly/Exception.h>
 #include <folly/ScopeGuard.h>
+#include <folly/Shell.h>
 #include <folly/String.h>
 #include <folly/io/Cursor.h>
 #include <folly/portability/Environment.h>
@@ -183,26 +184,10 @@ Subprocess::Subprocess(
     throw std::invalid_argument("usePath() not allowed when running in shell");
   }
 
-  auto argv = Subprocess::shellify(cmd);
+  auto argv = shellify(cmd);
   spawn(cloneStrings(argv), argv[0].c_str(), options, env);
 }
 
-/* static */ std::vector<std::string> Subprocess::shellify(
-    const std::string& cmd) {
-  std::vector<std::string> argv;
-
-  const char* shell = getenv("SHELL");
-  if (!shell) {
-    shell = "/bin/sh";
-  }
-
-  argv.push_back(shell);
-  argv.push_back("-c");
-  argv.push_back(cmd);
-
-  return argv;
-}
-
 Subprocess::~Subprocess() {
   CHECK_NE(returnCode_.state(), ProcessReturnCode::RUNNING)
     << "Subprocess destroyed without reaping child";
index 0300fd8efdeac65f711e2fb5c503ccf83535671d..a3b9f877682013f588fc7ac3ccf3a11a09e25fd9 100644 (file)
@@ -462,8 +462,6 @@ class Subprocess {
       const Options& options = Options(),
       const std::vector<std::string>* env = nullptr);
 
-  static std::vector<std::string> shellify(const std::string& cmd);
-
   ////
   //// The methods below only manipulate the process state, and do not
   //// affect its communication pipes.
diff --git a/folly/test/ShellTest.cpp b/folly/test/ShellTest.cpp
new file mode 100644 (file)
index 0000000..d129d5b
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <glog/logging.h>
+#include <gtest/gtest.h>
+
+#include <folly/Shell.h>
+
+using namespace folly;
+
+TEST(Shell, ShellQuote) {
+  EXPECT_EQ(shellQuote("a"), "'a'");
+  EXPECT_EQ(shellQuote("a'b"), "'a'\\''b'");
+  EXPECT_EQ(shellQuote("a\"b"), "'a\"b'");
+}
+
+TEST(Shell, Shellify) {
+  auto command = shellify("rm -rf /");
+  EXPECT_EQ(command[1], "-c");
+  EXPECT_EQ(command[2], "rm -rf /");
+
+  command = shellify("rm -rf {}", "someFile.txt");
+  EXPECT_EQ(command[2], "rm -rf 'someFile.txt'");
+
+  command = shellify("rm -rf {}", 5);
+  EXPECT_EQ(command[2], "rm -rf '5'");
+
+  command = shellify("ls {}", "blah'; rm -rf /");
+  EXPECT_EQ(command[2], "ls 'blah'\\''; rm -rf /'");
+}
index 419db0689d569dd36e207d4d5340c335d33c8644..66b1831957420556ca4c514deb3aaa919c65dcf7 100644 (file)
@@ -531,9 +531,3 @@ TEST(CommunicateSubprocessTest, TakeOwnershipOfPipes) {
   buf[2] = 0;
   EXPECT_EQ("3\n", std::string(buf));
 }
-
-TEST(Subprocess, Shellify) {
-  auto argv = Subprocess::shellify("rm -rf /");
-  EXPECT_EQ(argv[1], "-c");
-  EXPECT_EQ(argv[2], "rm -rf /");
-}