From 1335d05f7c2340cc04c12d592ec0dc8b40a3cab0 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 28 May 2026 14:50:00 +0200 Subject: [PATCH] Fix to harden GRUB manual editor allowlist --- grub2/grub2-lib.pl | 18 ++++++++++++++++-- grub2/t/run-tests.t | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/grub2/grub2-lib.pl b/grub2/grub2-lib.pl index 80a2eab8e..7712bcb3d 100644 --- a/grub2/grub2-lib.pl +++ b/grub2/grub2-lib.pl @@ -1406,7 +1406,7 @@ if ($grub_dir ne '' && -d $grub_dir && opendir(my $dh, $grub_dir)) { # Hide dotfiles and only expose regular generator/text files. next if ($base =~ /^\./); my $file = "$grub_dir/$base"; - next if (!-f $file); + next if (!&grub2_manual_file_safe($file, $grub_dir, 1)); my $type = defined($custom_file) && $file eq $custom_file ? 'custom' : (-x $file || $base =~ /^\d+_/) ? 'grub_script' : 'text'; @@ -1422,7 +1422,7 @@ if ($bls_dir ne '' && -d $bls_dir && opendir(my $dh, $bls_dir)) { # Disabled rescue files deliberately do not appear in the editor. next if ($base =~ /^\./ || $base !~ /\.conf\z/); my $file = "$bls_dir/$base"; - next if (!-f $file); + next if (!&grub2_manual_file_safe($file, $bls_dir, 1)); &add_grub2_manual_file(\@files, \%seen, 'bls_dir', $file, 'bls'); } @@ -1438,9 +1438,23 @@ sub add_grub2_manual_file { my ($files, $seen, $key, $file, $type) = @_; return if (!defined($file) || $file eq '' || $seen->{$file}++); +return if (!&grub2_manual_file_safe($file, undef, 0)); push(@$files, { 'key' => $key, 'file' => $file, 'type' => $type }); } +# grub2_manual_file_safe(file, [parent-dir], must-exist?) +# Returns true if a path is safe for the manual editor allowlist. +sub grub2_manual_file_safe +{ +my ($file, $parent, $must_exist) = @_; +return 0 if (!defined($file) || $file eq ''); +my @st = lstat($file); +return $must_exist ? 0 : 1 if (!@st); +# Symlinks would be followed by the Webmin write path, so reject them here. +return 0 if (-l _ || !-f _); +return $parent ? &grub2_path_is_under($file, $parent) : 1; +} + # grub2_manual_file(file) # Returns the manual-edit descriptor for an allowed file path. sub grub2_manual_file diff --git a/grub2/t/run-tests.t b/grub2/t/run-tests.t index 0304da255..7a7fc5d81 100644 --- a/grub2/t/run-tests.t +++ b/grub2/t/run-tests.t @@ -677,6 +677,35 @@ ok(grub2_manual_file($os_prober_file), 'grub.d script is manual-edit allowlisted ok(grub2_manual_file($readme_file), 'grub.d regular file is manual-edit allowlisted'); ok(grub2_manual_file($bls_entry_file), 'BLS entry is manual-edit allowlisted'); ok(!grub2_manual_file("$work/not-allowed"), 'unexpected file is rejected'); +SKIP: { + my $outside_manual = "$work/outside-manual-target"; + my $manual_link = "$work/grub.d/09_symlink"; + write_test_file($outside_manual, "outside\n"); + skip 'symlink unavailable', 5 if (!symlink($outside_manual, $manual_link)); + ok(!grub2_manual_file($manual_link), + 'grub.d symlink is not manual-edit allowlisted'); + is(save_manual_grub_file($manual_link, "#!/bin/sh\nexit 0\n"), + $text{'manual_efile'}, 'manual save rejects grub.d symlink'); + is(slurp_test_file($outside_manual), "outside\n", + 'manual save does not write through grub.d symlink'); + + my $outside_bls = "$work/outside-bls-target"; + my $bls_link = "$bls_dir/symlink.conf"; + write_test_file($outside_bls, "title Outside\nlinux /vmlinuz\n"); + if (!symlink($outside_bls, $bls_link)) { + unlink($manual_link); + skip 'second symlink unavailable', 2; + } + ok(!grub2_manual_file($bls_link), + 'BLS symlink is not manual-edit allowlisted'); + { + local $config{'custom_file'} = $manual_link; + ok(!grub2_manual_file($manual_link), + 'configured custom symlink is not manual-edit allowlisted'); + } + unlink($manual_link); + unlink($bls_link); +} is(save_manual_grub_file($default_file, $saved), undef, 'manual save validates default file'); is(save_manual_grub_file($custom_file, "menuentry 'X' { true }\n"), undef,