CVE-2022-3287 (published 2022-09-28T20:15Z)

When creating an OPERATOR user account on the BMC, the redfish plugin saved the auto-generated password to /etc/fwupd/redfish.conf without proper restriction, allowing any user on the system to read the same configuration file.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-552
cwe_name: Files or Directories Accessible to External Parties
description: The product makes files or directories accessible to unauthorized actors, even though they should not be.
extended_description: 
	   Web servers, FTP servers, and similar servers may store a set of files underneath a "root" directory that is accessible to the server's users.  Applications may store sensitive files underneath this root without also using access control to limit which users may request those files, if any.  Alternately, an application might package multiple files or directories into an archive file (e.g., ZIP or tar), but the application might not exclude sensitive files that are underneath those directories.
	   In cloud technologies and containers, this weakness might present itself in the form of misconfigured storage accounts that can be read or written by a public or anonymous user.
	 
         

Fix information:
repo_url: https://github.com/fwupd/fwupd
commit_details: repo_url: https://github.com/fwupd/fwupd
author: Richard Hughes
author_date: 2022-09-21 14:56:10+01:00
msg: Never save the Redfish passwords to a file readable by users

When the redfish plugin automatically creates an OPERATOR user account on the
BMC we save the autogenerated password to /etc/fwupd/redfish.conf, ensuring it
is chmod'ed to 0660 before writing the file with g_key_file_save_to_file().

Under the covers, g_key_file_save_to_file() calls g_file_set_contents() with
the keyfile string data.
I was under the impression that G_FILE_CREATE_REPLACE_DESTINATION was being
used to copy permissions, but alas not.

GLib instead calls g_file_set_contents_full() with the mode hardcoded to 0666,
which undoes the previous chmod().

Use g_file_set_contents_full() with the correct mode for newer GLib versions,
and provide a fallback with the same semantics for older versions.
repository: repo_url: https://github.com/fwupd/fwupd
repo_name: fwupd/fwupd
description: A system daemon to allow session software to update firmware
repo_language: C
owner: fwupd
file_changes: filename: fwupd.spec.in
change_type: ModificationType.MODIFY
diff: @@ -326,6 +326,9 @@ for fn in /etc/fwupd/remotes.d/*.conf; do
     fi
 done
 
+# ensure this is private
+chmod 0660 /etc/fwupd/redfish.conf
+
 %preun
 %systemd_preun fwupd.service
 

programming_language: Shell
filename: fu-plugin.c
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@
 #include "config.h"
 
 #include <errno.h>
+#include <fcntl.h>
 #include <fwupd.h>
 #include <glib/gstdio.h>
 #include <gmodule.h>
@@ -2417,6 +2418,46 @@ fu_plugin_set_config_value(FuPlugin *self, const gchar *key, const gchar *value,
 	return g_key_file_save_to_file(keyfile, conf_path, error);
 }
 
+#if !GLIB_CHECK_VERSION(2, 66, 0)
+
+#define G_FILE_SET_CONTENTS_CONSISTENT 0
+typedef guint GFileSetContentsFlags;
+static gboolean
+g_file_set_contents_full(const gchar *filename,
+			 const gchar *contents,
+			 gssize length,
+			 GFileSetContentsFlags flags,
+			 int mode,
+			 GError **error)
+{
+	gint fd;
+	gssize wrote;
+
+	if (length < 0)
+		length = strlen(contents);
+	fd = g_open(filename, O_CREAT, mode);
+	if (fd <= 0) {
+		g_set_error(error,
+			    G_IO_ERROR,
+			    G_IO_ERROR_FAILED,
+			    "could not open %s file",
+			    filename);
+		return FALSE;
+	}
+	wrote = write(fd, contents, length);
+	if (wrote != length) {
+		g_set_error(error,
+			    G_IO_ERROR,
+			    G_IO_ERROR_FAILED,
+			    "did not write %s file",
+			    filename);
+		g_close(fd, NULL);
+		return FALSE;
+	}
+	return g_close(fd, error);
+}
+#endif
+
 /**
  * fu_plugin_set_secure_config_value:
  * @self: a #FuPlugin
@@ -2438,7 +2479,8 @@ fu_plugin_set_secure_config_value(FuPlugin *self,
 				  GError **error)
 {
 	g_autofree gchar *conf_path = fu_plugin_get_config_filename(self);
-	gint ret;
+	g_autofree gchar *data = NULL;
+	g_autoptr(GKeyFile) keyfile = g_key_file_new();
 
 	g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE);
 	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
@@ -2447,17 +2489,18 @@ fu_plugin_set_secure_config_value(FuPlugin *self,
 		g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s is missing", conf_path);
 		return FALSE;
 	}
-	ret = g_chmod(conf_path, 0660);
-	if (ret == -1) {
-		g_set_error(error,
-			    FWUPD_ERROR,
-			    FWUPD_ERROR_INTERNAL,
-			    "failed to set permissions on %s",
-			    conf_path);
+	if (!g_key_file_load_from_file(keyfile, conf_path, G_KEY_FILE_KEEP_COMMENTS, error))
 		return FALSE;
-	}
-
-	return fu_plugin_set_config_value(self, key, value, error);
+	g_key_file_set_string(keyfile, fu_plugin_get_name(self), key, value);
+	data = g_key_file_to_data(keyfile, NULL, error);
+	if (data == NULL)
+		return FALSE;
+	return g_file_set_contents_full(conf_path,
+					data,
+					-1,
+					G_FILE_SET_CONTENTS_CONSISTENT,
+					0660,
+					error);
 }
 
 /**

programming_language: C
filename: fu-self-test.c
change_type: ModificationType.MODIFY
diff: @@ -674,6 +674,62 @@ _plugin_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data)
 	fu_test_loop_quit();
 }
 
+static void
+fu_plugin_config_func(void)
+{
+	GStatBuf statbuf = {0};
+	gboolean ret;
+	gint rc;
+	g_autofree gchar *conf_dir = NULL;
+	g_autofree gchar *conf_file = NULL;
+	g_autofree gchar *fn = NULL;
+	g_autofree gchar *testdatadir = NULL;
+	g_autofree gchar *value = NULL;
+	g_autoptr(FuPlugin) plugin = fu_plugin_new(NULL);
+	g_autoptr(GError) error = NULL;
+
+	/* this is a build file */
+	testdatadir = g_test_build_filename(G_TEST_BUILT, "tests", NULL);
+	(void)g_setenv("FWUPD_SYSCONFDIR", testdatadir, TRUE);
+	conf_dir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG);
+
+	/* remove existing file */
+	fu_plugin_set_name(plugin, "test");
+	conf_file = g_strdup_printf("%s.conf", fu_plugin_get_name(plugin));
+	fn = g_build_filename(conf_dir, conf_file, NULL);
+	ret = fu_path_mkdir_parent(fn, &error);
+	g_assert_no_error(error);
+	g_assert_true(ret);
+	g_remove(fn);
+	ret = g_file_set_contents(fn, "", -1, &error);
+	g_assert_no_error(error);
+	g_assert_true(ret);
+
+	/* set a value */
+	ret = fu_plugin_set_config_value(plugin, "Key", "True", &error);
+	g_assert_no_error(error);
+	g_assert_true(ret);
+	g_assert_true(g_file_test(fn, G_FILE_TEST_EXISTS));
+
+	/* check it is world readable */
+	rc = g_stat(fn, &statbuf);
+	g_assert_cmpint(rc, ==, 0);
+	g_assert_cmpint(statbuf.st_mode & 0777, ==, 0644);
+
+	/* read back the value */
+	value = fu_plugin_get_config_value(plugin, "Key");
+	g_assert_cmpstr(value, ==, "True");
+	g_assert_true(fu_plugin_get_config_value_boolean(plugin, "Key"));
+
+	/* check it is private, i.e. only readable by the user/group */
+	ret = fu_plugin_set_secure_config_value(plugin, "Key", "False", &error);
+	g_assert_no_error(error);
+	g_assert_true(ret);
+	rc = g_stat(fn, &statbuf);
+	g_assert_cmpint(rc, ==, 0);
+	g_assert_cmpint(statbuf.st_mode & 0777, ==, 0640);
+}
+
 static void
 fu_plugin_devices_func(void)
 {
@@ -3598,6 +3654,7 @@ main(int argc, char **argv)
 	g_test_add_func("/fwupd/progress{finish}", fu_progress_finish_func);
 	g_test_add_func("/fwupd/bios-attrs{load}", fu_bios_settings_load_func);
 	g_test_add_func("/fwupd/security-attrs{hsi}", fu_security_attrs_hsi_func);
+	g_test_add_func("/fwupd/plugin{config}", fu_plugin_config_func);
 	g_test_add_func("/fwupd/plugin{devices}", fu_plugin_devices_func);
 	g_test_add_func("/fwupd/plugin{device-inhibit-children}",
 			fu_plugin_device_inhibit_children_func);

programming_language: C