Vulnerability: The Trigger plugin in bcfg2 1.2.x before 1.2.3 allows remote attackers with root access to the client to execute arbitrary commands via shell metacharacters in the UUID field to the server process (bcfg2-server).
Vulnerability ID: CVE-2012-3366
CWE: CWE-78
Severity score: 10
Project: https://github.com/Bcfg2/bcfg2
Published: 2012-07-03
File: src/lib/Server/Plugins/Trigger.py
Commit message: fixed major security flaw in Trigger plugin
Commit author: Chris St. Pierre

Vulnerable version (before fix):
 import os import Bcfg2.Server.Plugin   def async_run(prog, args):     pid = os.fork()     if pid:         os.waitpid(pid, 0)     else:         dpid = os.fork()         if not dpid:             os.system(" ".join([prog] + args))         os._exit(0)   class Trigger(Bcfg2.Server.Plugin.Plugin,               Bcfg2.Server.Plugin.Statistics): def __init__(self, core, datastore):                               "unloading" % self.data)             raise Bcfg2.Server.Plugin.PluginInitError      def process_statistics(self, metadata, _):         args = [metadata.hostname, '-p', metadata.profile, '-g',                 ':'.join([g for g in metadata.groups])]         for notifier in os.listdir(self.data):             if ((notifier[-1] == '~') or                 (notifier[:2] == '.#') or                 (notifier[-4:] == '.swp') or                 (notifier in ['SCCS', '.svn', '4913'])):                 continue             npath = self.data + '/' + notifier             self.logger.debug("Running %s %s" % (npath, " ".join(args)))             async_run(npath, args)

Fixed version (after fix):
 import os import pipes import Bcfg2.Server.Plugin from subprocess import Popen, PIPE  class Trigger(Bcfg2.Server.Plugin.Plugin,               Bcfg2.Server.Plugin.Statistics): def __init__(self, core, datastore):                               "unloading" % self.data)             raise Bcfg2.Server.Plugin.PluginInitError      def async_run(self, args):         pid = os.fork()         if pid:             os.waitpid(pid, 0)         else:             dpid = os.fork()             if not dpid:                 self.debug_log("Running %s" % " ".join(pipes.quote(a)                                                        for a in args))                 proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)                 (out, err) = proc.communicate()                 rv = proc.wait()                 if rv != 0:                     self.logger.error("Trigger: Error running %s (%s): %s" %                                       (args[0], rv, err))                 elif err:                     self.debug_log("Trigger: Error: %s" % err)             os._exit(0)      def process_statistics(self, metadata, _):         args = [metadata.hostname, '-p', metadata.profile, '-g',                 ':'.join([g for g in metadata.groups])]         self.debug_log("running triggers")         for notifier in os.listdir(self.data):             self.debug_log("running %s" % notifier)             if ((notifier[-1] == '~') or                 (notifier[:2] == '.#') or                 (notifier[-4:] == '.swp') or                 (notifier in ['SCCS', '.svn', '4913'])):                 continue             npath = os.path.join(self.data, notifier)             self.async_run([npath] + args)

Patch:
patch: @@ -1,17 +1,7 @@
 import os
+import pipes
 import Bcfg2.Server.Plugin
-
-
-def async_run(prog, args):
-    pid = os.fork()
-    if pid:
-        os.waitpid(pid, 0)
-    else:
-        dpid = os.fork()
-        if not dpid:
-            os.system(" ".join([prog] + args))
-        os._exit(0)
-
+from subprocess import Popen, PIPE
 
 class Trigger(Bcfg2.Server.Plugin.Plugin,
               Bcfg2.Server.Plugin.Statistics):
@@ -30,15 +20,35 @@ def __init__(self, core, datastore):
                               "unloading" % self.data)
             raise Bcfg2.Server.Plugin.PluginInitError
 
+    def async_run(self, args):
+        pid = os.fork()
+        if pid:
+            os.waitpid(pid, 0)
+        else:
+            dpid = os.fork()
+            if not dpid:
+                self.debug_log("Running %s" % " ".join(pipes.quote(a)
+                                                       for a in args))
+                proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+                (out, err) = proc.communicate()
+                rv = proc.wait()
+                if rv != 0:
+                    self.logger.error("Trigger: Error running %s (%s): %s" %
+                                      (args[0], rv, err))
+                elif err:
+                    self.debug_log("Trigger: Error: %s" % err)
+            os._exit(0)
+
     def process_statistics(self, metadata, _):
         args = [metadata.hostname, '-p', metadata.profile, '-g',
                 ':'.join([g for g in metadata.groups])]
+        self.debug_log("running triggers")
         for notifier in os.listdir(self.data):
+            self.debug_log("running %s" % notifier)
             if ((notifier[-1] == '~') or
                 (notifier[:2] == '.#') or
                 (notifier[-4:] == '.swp') or
                 (notifier in ['SCCS', '.svn', '4913'])):
                 continue
-            npath = self.data + '/' + notifier
-            self.logger.debug("Running %s %s" % (npath, " ".join(args)))
-            async_run(npath, args)
+            npath = os.path.join(self.data, notifier)
+            self.async_run([npath] + args)
path: src/lib/Server/Plugins/Trigger.py