Date: Thu, 14 May 2026 19:07:18 +0000 From: Colin Percival <cperciva@FreeBSD.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org Cc: Baptiste Daroussin <bapt@FreeBSD.org> Subject: git: 2ebec3c7ead5 - releng/15.1 - nuageinit: fix command injection and related issues Message-ID: <6a061d66.25a30.368aaaca@gitrepo.freebsd.org>
index | next in thread | raw e-mail
The branch releng/15.1 has been updated by cperciva: URL: https://cgit.FreeBSD.org/src/commit/?id=2ebec3c7ead5f5b477a5ab47136562777f2bb495 commit 2ebec3c7ead5f5b477a5ab47136562777f2bb495 Author: Baptiste Daroussin <bapt@FreeBSD.org> AuthorDate: 2026-05-07 18:22:14 +0000 Commit: Colin Percival <cperciva@FreeBSD.org> CommitDate: 2026-05-14 19:06:56 +0000 nuageinit: fix command injection and related issues - Add shell_escape() helper to safely escape shell arguments - Apply shell_escape to all user-controlled values in shell commands: adduser (usershow, useradd, lock, primary_group, groups) addgroup (groupshow, groupadd, members) exec_change_password (usermod) settimezone (tzsetup root and timezone) install_package (pkg package names) - Escape double quotes in hostname when writing rc.conf.d/hostname - Add missing 'local' declaration for resolvconf_command in nameservers() - Escape interface name in resolvconf -a command - Change open_resolvconf_conf() from 'w' to 'a' mode to prevent data loss when nameservers() is called multiple times - Clean up stale resolvconf.conf at the start of each boot (skip on postnet to preserve config written by first call) Approved by: re (cperciva) MFC After: 1 day (cherry picked from commit 8b70a203be10411c560ed303ab25713d70b316e9) (cherry picked from commit 87b18b611ec9a70347fdd239345fa23977bcb2d0) --- libexec/nuageinit/nuage.lua | 43 +++++++++++++++++++++++------------- libexec/nuageinit/nuageinit | 17 ++++++++++++-- libexec/nuageinit/tests/nuageinit.sh | 6 ++--- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index 2d962b540b23..f3c23a7c3eb8 100644 --- a/libexec/nuageinit/nuage.lua +++ b/libexec/nuageinit/nuage.lua @@ -52,6 +52,10 @@ local function decode_base64(input) return table.concat(result) end +local function shell_escape(s) + return "'" .. string.gsub(s, "'", "'\\''") .. "'" +end + local function warnmsg(str, prepend) if not str then return @@ -121,7 +125,7 @@ local function sethostname(hostname) warnmsg("Impossible to open " .. hostnamepath .. ":" .. err) return end - f:write('hostname="' .. hostname .. '"\n') + f:write('hostname="' .. hostname:gsub('"', '\\"') .. '"\n') f:close() end @@ -199,7 +203,7 @@ local function adduser(pwd) if root then cmd = cmd .. "-R " .. root .. " " end - local f = io.popen(cmd .. " usershow " .. pwd.name .. " -7 2> /dev/null") + local f = io.popen(cmd .. " usershow " .. shell_escape(pwd.name) .. " -7 2> /dev/null") local pwdstr = f:read("*a") f:close() if pwdstr:len() ~= 0 then @@ -220,13 +224,17 @@ local function adduser(pwd) -- a warning but creates the user anyway. list = purge_group(list) if #list > 0 then - extraargs = " -G " .. table.concat(list, ",") + local escaped_list = {} + for _, g in ipairs(list) do + table.insert(escaped_list, shell_escape(g)) + end + extraargs = " -G " .. table.concat(escaped_list, ",") end end -- pw will automatically create a group named after the username -- do not add a -g option in this case if pwd.primary_group and pwd.primary_group ~= pwd.name then - extraargs = extraargs .. " -g " .. pwd.primary_group + extraargs = extraargs .. " -g " .. shell_escape(pwd.primary_group) end if not pwd.no_create_home then extraargs = extraargs .. " -m " @@ -248,9 +256,9 @@ local function adduser(pwd) if root then cmd = cmd .. "-R " .. root .. " " end - cmd = cmd .. "useradd -n " .. pwd.name .. " -M 0755 -w none " - cmd = cmd .. extraargs .. " -c '" .. pwd.gecos - cmd = cmd .. "' -d '" .. pwd.homedir .. "' -s " .. pwd.shell .. postcmd + cmd = cmd .. "useradd -n " .. shell_escape(pwd.name) .. " -M 0755 -w none " + cmd = cmd .. extraargs .. " -c " .. shell_escape(pwd.gecos) + cmd = cmd .. " -d " .. shell_escape(pwd.homedir) .. " -s " .. shell_escape(pwd.shell) .. postcmd f = io.popen(cmd, "w") if input then @@ -267,7 +275,7 @@ local function adduser(pwd) if root then cmd = cmd .. "-R " .. root .. " " end - cmd = cmd .. "lock " .. pwd.name + cmd = cmd .. "lock " .. shell_escape(pwd.name) os.execute(cmd) end return pwd.homedir @@ -283,7 +291,7 @@ local function addgroup(grp) if root then cmd = cmd .. "-R " .. root .. " " end - local f = io.popen(cmd .. " groupshow " .. grp.name .. " 2> /dev/null") + local f = io.popen(cmd .. " groupshow " .. shell_escape(grp.name) .. " 2> /dev/null") local grpstr = f:read("*a") f:close() if grpstr:len() ~= 0 then @@ -292,13 +300,17 @@ local function addgroup(grp) local extraargs = "" if grp.members then local list = splitlist(grp.members) - extraargs = " -M " .. table.concat(list, ",") + local escaped_list = {} + for _, m in ipairs(list) do + table.insert(escaped_list, shell_escape(m)) + end + extraargs = " -M " .. table.concat(escaped_list, ",") end cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end - cmd = cmd .. "groupadd -n " .. grp.name .. extraargs + cmd = cmd .. "groupadd -n " .. shell_escape(grp.name) .. extraargs local r = os.execute(cmd) if not r then warnmsg("fail to add group " .. grp.name) @@ -484,7 +496,7 @@ local function exec_change_password(user, password, type, expire) postcmd = " -w random" end end - cmd = cmd .. "usermod " .. user .. postcmd + cmd = cmd .. "usermod " .. shell_escape(user) .. postcmd if expire then cmd = cmd .. " -p 1" else @@ -577,7 +589,7 @@ local function settimezone(timezone) root = "/" end - local f, _, rc = os.execute("tzsetup -s -C " .. root .. " " .. timezone) + local f, _, rc = os.execute("tzsetup -s -C " .. shell_escape(root) .. " " .. shell_escape(timezone)) if not f then warnmsg("Impossible to configure time zone ( rc = " .. rc .. " )") @@ -600,8 +612,8 @@ local function install_package(package) if package == nil then return true end - local install_cmd = "pkg install -y " .. package - local test_cmd = "pkg info -q " .. package + local install_cmd = "pkg install -y " .. shell_escape(package) + local test_cmd = "pkg info -q " .. shell_escape(package) if os.getenv("NUAGE_RUN_TESTS") then print(install_cmd) print(test_cmd) @@ -683,6 +695,7 @@ local function addfile(file, defer) end local n = { + shell_escape = shell_escape, warn = warnmsg, err = errmsg, chmod = chmod, diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index a1ebd3f52b25..fc8d9582b9c6 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -67,7 +67,14 @@ local function open_resolv_conf() end local function open_resolvconf_conf() - return openat("/etc", "resolvconf.conf") + local path_dir = root .. "/etc" + local path_name = path_dir .. "/resolvconf.conf" + nuage.mkdir_p(path_dir) + local f, err = io.open(path_name, "a") + if not f then + nuage.err("unable to open " .. path_name .. ": " .. err) + end + return f, path_name end local function get_ifaces_by_mac() @@ -271,8 +278,9 @@ local function nameservers(interface, obj) end -- Only call resolvconf with interface if interface is provided + local resolvconf_command if interface then - resolvconf_command = "resolvconf -a " .. interface .. " < " .. resolv_conf + resolvconf_command = "resolvconf -a " .. nuage.shell_escape(interface) .. " < " .. resolv_conf else resolvconf_command = "resolvconf -u" end @@ -738,6 +746,11 @@ local function load_userdata() return line, obj end +-- Clean up stale resolvconf.conf from previous boot +if citype ~= "postnet" then + os.remove(root .. "/etc/resolvconf.conf") +end + if citype == "config-2" then -- network config2_network(ni_path) diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh index 3a01413f8487..9f7dc7d38a3c 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -799,7 +799,7 @@ packages: - yeah/plop EOF chmod 755 "${PWD}"/media/nuageinit/user_data - atf_check -s exit:0 -o inline:"pkg install -y yeah/plop\npkg info -q yeah/plop\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet + atf_check -s exit:0 -o inline:"pkg install -y 'yeah/plop'\npkg info -q 'yeah/plop'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet cat > media/nuageinit/user_data << 'EOF' #cloud-config @@ -807,7 +807,7 @@ packages: - curl EOF chmod 755 "${PWD}"/media/nuageinit/user_data - atf_check -o inline:"pkg install -y curl\npkg info -q curl\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet + atf_check -o inline:"pkg install -y 'curl'\npkg info -q 'curl'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet cat > media/nuageinit/user_data << 'EOF' #cloud-config @@ -816,7 +816,7 @@ packages: - meh: bla EOF chmod 755 "${PWD}"/media/nuageinit/user_data - atf_check -o inline:"pkg install -y curl\npkg info -q curl\n" -e inline:"nuageinit: Invalid type: table for packages entry number 2\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet + atf_check -o inline:"pkg install -y 'curl'\npkg info -q 'curl'\n" -e inline:"nuageinit: Invalid type: table for packages entry number 2\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_update_packages_body()home | help
Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a061d66.25a30.368aaaca>
