From 4af4392bc3f2c37784d11a6b49cb1b9eb7d9c3d9 Mon Sep 17 00:00:00 2001 From: Ashlen Date: Wed, 15 Jul 2026 12:15:22 -0600 Subject: [PATCH 1/2] fix(permission-hardener): prevent fail-open on empty matchwhitelist check_nosuid_whitelist iterated with "${policy_match_white_list[@]:-}". Under `set -o nounset` the `:-` default expands an empty array to a single empty-string element, so the loop ran once with an empty entry. The body test `[[ "${target_file}" == *""* ]]` matches every string, so the function returned 1 (whitelisted) for every file. Both callers use `|| continue`, so an effective config with no `matchwhitelist` line silently skipped all SUID/SGID hardening with exit 0 -- a fail-open. Expand with ${arr[@]+"${arr[@]}"} instead, which yields no elements when the array is empty and does not trip nounset. --- usr/bin/permission-hardener#security-misc-shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 19b43f56..c91823bb 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -280,7 +280,7 @@ check_nosuid_whitelist() { ## literal matching is intentional here too [[ " ${policy_exact_white_list[*]} " =~ " ${target_file} " ]] && return 1 - for match_white_list_entry in "${policy_match_white_list[@]:-}"; do + for match_white_list_entry in ${policy_match_white_list[@]+"${policy_match_white_list[@]}"}; do if [[ "${target_file}" == *"${match_white_list_entry}"* ]]; then return 1 fi From 1625a3db8fbb94da8c5558c52d9b7b481ce031cd Mon Sep 17 00:00:00 2001 From: Ashlen Date: Wed, 15 Jul 2026 12:15:57 -0600 Subject: [PATCH 2/2] fix(permission-hardener): skip hardlinked files instead of aborting output_stat returned 1 for a non-directory SUID/SGID file with a hardlink count > 1. The four call sites invoke output_stat as a bare command under `set -o errexit`, so that return propagated and terminated the whole run instead of skipping the file. The following `if [ -z "${file_name_from_stat}" ]` guard never caught this case because file_name_from_stat was already populated. The surrounding comment states hardlinked files are meant to be skipped (scanning a hardlink pool is too costly). Blank the output vars and return 0, matching the existing nonexistent-file skip path, so the caller's empty-name check continues past the file. Genuine error paths (stat failure, parse corruption) still return 1 and fail loud. --- usr/bin/permission-hardener#security-misc-shared | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index c91823bb..f2fa5080 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -188,7 +188,11 @@ File name: '${file_name}' File name from stat: '${file_name_from_stat}' line: '${processed_config_line}' " >&2 - return 1 + existing_mode='' + existing_owner='' + existing_group='' + file_name_from_stat='' + return 0 fi fi