CVE-2022-4968 (C, published 2024-06-07T01:15Z)
Description: value: netplan leaks the private key of wireguard to local users. Versions after 1.0 are not affected.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/canonical/netplan
Files changed:
src/networkd.c
src/networkd.h
src/nm.c
src/openvswitch.c
src/sriov.c
src/util-internal.h
src/util.c
tests/generator/test_auth.py
tests/generator/test_wifis.py
tests/integration/base.py
Commit message: libnetplan: use more restrictive file permissions

A new util.c:_netplan_g_string_free_to_file_with_permissions() was added
and accepts the owner, group and file mode as arguments. When these
properties can't be set, when the generator is called by a non-root user
for example, it will not hard-fail. This function is called by unit
tests where we can't set the owner to a privileged account for example.

When generating backend files, use more restrictive permissions:

networkd related files will be owned by root:systemd-network and have
mode 0640.

service unit files will be owned by root:root and have mode 0640.
udevd files will be owned by root:root with mode 0640.

wpa_supplicant and Network Manager files will continue with the existing
permissions.

Autopkgtests will check if the permissions are set as expected when
calling the generator.

This fix addresses CVE-2022-4968


Vulnerable code:
    mode_t orig_umask;
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, ".link");
    umask(orig_umask);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    mode_t orig_umask;
    /* these do not contain secrets and need to be readable by
     * systemd-networkd - LP: #1736965 */
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, ".netdev");
    umask(orig_umask);
    mode_t orig_umask;
        /* these do not contain secrets and need to be readable by
         * systemd-networkd - LP: #1736965 */
        orig_umask = umask(022);
        _netplan_g_string_free_to_file(s, rootdir, path, ".network");
        umask(orig_umask);
    mode_t orig_umask;
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    umask(orig_umask);
    mode_t orig_umask;
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    umask(orig_umask);
    mode_t orig_umask;
    orig_umask = umask(077);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    umask(orig_umask);
        _netplan_g_string_free_to_file(content, rootdir, override, NULL);
    _netplan_g_string_free_to_file(content, rootdir, override, NULL);
        _netplan_g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
        _netplan_g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)

Fixed code:
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
        _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
        _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
#define NETWORKD_GROUP "systemd-network"

        _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
        _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
void
_netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode);

#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
void _netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode)
{
    g_autofree char* full_path = NULL;
    g_autofree char* path_suffix = NULL;
    g_autofree char* contents = g_string_free(s, FALSE);
    GError* error = NULL;
    struct passwd* pw = NULL;
    struct group* gr = NULL;
    int ret = 0;

    path_suffix = g_strjoin(NULL, path, suffix, NULL);
    full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
    _netplan_safe_mkdir_p_dir(full_path);
    if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
        /* the mkdir() just succeeded, there is no sensible
         * method to test this without root privileges, bind mounts, and
         * simulating ENOSPC */
        // LCOV_EXCL_START
        g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
        exit(1);
        // LCOV_EXCL_STOP
    }

    /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
     * It's OK to fail to set the owners and mode as this code will be called from unit tests.
     * The autopkgtests will check if the owner/group and mode are correctly set.
     */
    pw = getpwnam(owner);
    if (!pw) {
        g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
    }
    gr = getgrnam(group);
    if (!gr) {
        g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
    }
    if (pw && gr) {
        ret = chown(full_path, pw->pw_uid, gr->gr_gid);
        if (ret != 0) {
            g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
        }
    }
}

            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
import pwd
import grp
        # Assert file permissions
        self.assert_file_permissions()

    def assert_file_permissions(self):
        """ Check if the generated files have the expected permissions """

        nd_expected_mode = 0o100640
        nd_expected_owner = 'root'
        nd_expected_group = 'systemd-network'

        sd_expected_mode = 0o100640
        sd_expected_owner = 'root'
        sd_expected_group = 'root'

        udev_expected_mode = 0o100640
        udev_expected_owner = 'root'
        udev_expected_group = 'root'

        nm_expected_mode = 0o100600
        nm_expected_owner = 'root'
        nm_expected_group = 'root'

        wpa_expected_mode = 0o100600
        wpa_expected_owner = 'root'
        wpa_expected_group = 'root'

        # Check systemd-networkd files
        base_path = '/run/systemd/network'
        files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')

        # Check Network Manager files
        base_path = '/run/NetworkManager/system-connections'
        files = glob.glob(f'{base_path}/*.nmconnection')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')

        # Check wpa_supplicant configuration files
        base_path = '/run/netplan'
        files = glob.glob(f'{base_path}/wpa-*.conf')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')

        # Check systemd service unit files
        base_path = '/run/systemd/system/'
        files = glob.glob(f'{base_path}/netplan-*.service')
        files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')

        # Check systemd-udevd files
        udev_path = '/run/udev/rules.d'
        link_path = '/run/systemd/network'
        files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')