Compare commits

...

24 Commits

Author SHA1 Message Date
Ilia Ross
92fda35d10 Fix overflow for open dropdowns in hidden tables
Some checks are pending
Tests / prove (push) Waiting to run
Package and upload artifacts / build (push) Waiting to run
2026-11-04 01:20:51 +01:00
Ilia Ross
7820556044 Fix list link styling 2026-11-04 01:20:28 +01:00
Ilia Ross
4a2fb26f96 Add small styles for cluster elements 2026-11-04 00:51:33 +01:00
Ilia Ross
1693771f3f Fix choice container vertical padding
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-11-03 04:36:47 +01:00
Ilia Ross
abc62ae37f Fix optional RAID handling in LVM and partition modules 2026-11-03 02:01:24 +01:00
Ilia Ross
1170a5a35f Fix missing French nav label
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-11-03 01:15:13 +01:00
Ilia Ross
e9528e85a8 Fix titles in File Manager module 2026-11-03 01:14:55 +01:00
Jamie Cameron
01b988dbf1 Add missing escaping in shorewall modules
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
https://github.com/webmin/webmin/issues/2908
2026-11-01 16:45:18 -08:00
Ilia Ross
3f8a6afb3e 2.671
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-11-01 18:34:42 +01:00
Ilia Ross
c2019db310 Update tests
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-31 18:59:43 +01:00
Ilia Ross
22ac790256 Fix HTML escaping in user module validation errors 2026-10-31 18:59:38 +01:00
Ilia Ross
e01f104fcb Fix HTML escaping in process signal errors 2026-10-31 18:59:19 +01:00
Ilia Ross
e3f36c5d39 Update tests 2026-10-31 18:39:30 +01:00
Ilia Ross
053a1b28a7 Fix preservation of restricted group modules 2026-10-31 18:39:22 +01:00
Ilia Ross
47d0ac01b0 Update tests 2026-10-31 18:04:27 +01:00
Ilia Ross
9924545759 Fix process output escaping 2026-10-31 18:04:20 +01:00
Ilia Ross
8fb194e5fb Fix HTML escaping in certificate issuance responses 2026-10-31 18:02:00 +01:00
Ilia Ross
be54ba38c3 Fix group module validation before saving membership 2026-10-31 18:00:46 +01:00
Jamie Cameron
c861e308f4 Apply same module check when saving groups as is done for users 2026-10-31 09:19:39 -07:00
Jamie Cameron
c08b60c395 Add missing escaping of input 2026-10-31 09:09:24 -07:00
Jamie Cameron
0c449e3417 Disallow invalid characters in cert fields 2026-10-31 09:06:22 -07:00
Jamie Cameron
f0c3ba1d3c Only include security updates
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
https://github.com/webmin/webmin/issues/2906
2026-10-30 21:05:06 -07:00
Ilia Ross
99dc3b6281 Add plain text size handling for dropdown labels in device input 2026-10-31 01:30:01 +01:00
Ilia Ross
d8347b2928 Fix French navigation translations
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-29 21:08:11 +01:00
58 changed files with 367 additions and 76 deletions

View File

@@ -22,6 +22,7 @@ my $fh = "IN";
&open_tempfile($fh, ">$temp1"); &open_tempfile($fh, ">$temp1");
foreach my $k ("emailAddress", "organizationalUnitName", "organizationName", foreach my $k ("emailAddress", "organizationalUnitName", "organizationName",
"stateOrProvinceName", "countryName", "commonName") { "stateOrProvinceName", "countryName", "commonName") {
$in{$k} =~ /:|\n|\0/ && &error($text{'cert_echars'});
&print_tempfile($fh, "$k = $in{$k}\n"); &print_tempfile($fh, "$k = $in{$k}\n");
} }
$in{'key'} =~ s/\s//g; $in{'key'} =~ s/\s//g;
@@ -31,12 +32,12 @@ my $cmd = &get_ssleay();
my $ssleay = &backquote_logged("$cmd ca -spkac $temp1 -out $temp2 -config $module_config_directory/openssl.cnf -days 1095 2>&1"); my $ssleay = &backquote_logged("$cmd ca -spkac $temp1 -out $temp2 -config $module_config_directory/openssl.cnf -days 1095 2>&1");
&unlink_file($temp1); &unlink_file($temp1);
if ($?) { if ($?) {
&error("<pre>$ssleay</pre>"); &error("<pre>".&html_escape($ssleay)."</pre>");
} }
else { else {
# Display status and redirect to actual cert file # Display status and redirect to actual cert file
&ui_print_unbuffered_header(undef, $text{'cert_title'}, ""); &ui_print_unbuffered_header(undef, $text{'cert_title'}, "");
print &text('cert_done', $in{'commonName'}),"<p>\n"; print &text('cert_done', &html_escape($in{'commonName'})),"<p>\n";
print &text('cert_pickup', "cert_output.cgi?file=$temp2"),"<p>\n"; print &text('cert_pickup', "cert_output.cgi?file=$temp2"),"<p>\n";
&ui_print_footer("", $text{'index_return'}); &ui_print_footer("", $text{'index_return'});

View File

@@ -7,7 +7,7 @@ use warnings;
no warnings 'redefine'; no warnings 'redefine';
no warnings 'uninitialized'; no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access, $config_directory); our (%in, %text, %config, %access, $config_directory, $base_remote_user);
&ReadParse(); &ReadParse();
$access{'groups'} || &error($text{'gedit_ecannot'}); $access{'groups'} || &error($text{'gedit_ecannot'});
my $g; my $g;
@@ -95,6 +95,12 @@ print &ui_hidden_table_start($text{'edit_mods'}, "width=100%", 2, "mods");
# Show available modules, under categories # Show available modules, under categories
my @mlist = &list_module_infos(); my @mlist = &list_module_infos();
my %has = map { $_, 1 } @{$group{'modules'}}; my %has = map { $_, 1 } @{$group{'modules'}};
# Disable modules that this administrator cannot grant or remove.
my $me = $access{'mode'} == 1 ? &get_user($base_remote_user) : undef;
@mcan = $access{'mode'} == 1 ? @{$me->{'modules'} || []} :
$access{'mode'} == 2 ? split(/\s+/, $access{'mods'}) :
map { $_->{'dir'} } @mlist;
my %mcan = map { $_, 1 } @mcan;
my @links = ( &select_all_link("mod", 0, $text{'edit_selall'}), my @links = ( &select_all_link("mod", 0, $text{'edit_selall'}),
&select_invert_link("mod", 0, $text{'edit_invert'}) ); &select_invert_link("mod", 0, $text{'edit_invert'}) );
my @cats = &unique(map { $_->{'category'} || "" } @mlist); my @cats = &unique(map { $_->{'category'} || "" } @mlist);
@@ -119,7 +125,8 @@ foreach my $c (sort { $b cmp $a } @cats) {
else { else {
$label = $m->{'desc'}; $label = $m->{'desc'};
} }
push(@grid, &ui_checkbox("mod", $md, $label,$has{$md})); push(@grid, &ui_checkbox("mod", $md, $label, $has{$md},
undef, !$mcan{$md}));
} }
$grids .= &ui_grid_table(\@grid, 2, 100, [ "width=50%", "width=50%" ]); $grids .= &ui_grid_table(\@grid, 2, 100, [ "width=50%", "width=50%" ]);
} }

View File

@@ -175,6 +175,7 @@ cert_ekey=A new SSL key was not submitted by your browser - maybe it does not su
cert_eca=Failed to setup certificate authority : $1 cert_eca=Failed to setup certificate authority : $1
cert_already=Warning - you are already using the certificate $1. cert_already=Warning - you are already using the certificate $1.
cert_etempdir=Invalid certificate file cert_etempdir=Invalid certificate file
cert_echars=Invalid characters in cert field
acl_title=Module Access Control acl_title=Module Access Control
acl_title2=For $1 in $2 acl_title2=For $1 in $2

View File

@@ -7,7 +7,7 @@ use warnings;
no warnings 'redefine'; no warnings 'redefine';
no warnings 'uninitialized'; no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access, $config_directory); our (%in, %text, %config, %access, $config_directory, $base_remote_user);
&ReadParse(); &ReadParse();
$access{'groups'} || &error($text{'gedit_ecannot'}); $access{'groups'} || &error($text{'gedit_ecannot'});
@@ -21,6 +21,10 @@ elsif ($in{'but_delete'}) {
exit; exit;
} }
# Find logged-in webmin user
my @ulist = &list_users();
my ($me) = grep { $_->{'name'} eq $base_remote_user } @ulist;
my (%group, $old); my (%group, $old);
if ($in{'old'}) { if ($in{'old'}) {
# Get the original group # Get the original group
@@ -42,6 +46,21 @@ if (!$in{'old'} || $in{'old'} ne $in{'name'}) {
} }
$in{'desc'} !~ /:/ || &error($text{'gsave_edesc'}); $in{'desc'} !~ /:/ || &error($text{'gsave_edesc'});
# Check grantable modules before changing parent group membership.
$access{'mode'} == 1 && !$me && &error($text{'edit_egone'});
my @mcan = $access{'mode'} == 1 ? @{$me->{'modules'} || []} :
$access{'mode'} == 2 ? split(/\s+/, $access{'mods'}) :
&list_modules();
my %mcan = map { $_, 1 } @mcan;
# Keep existing modules that this administrator cannot change.
my %keep = map { $_, 1 } grep { !$mcan{$_} }
($old ? @{$old->{'modules'}} : ());
my @mods = split(/\0/, $in{'mod'});
foreach my $m (@mods) {
$mcan{$m} || $keep{$m} || &error(&text('save_emod', &html_escape($m)));
}
@mods = &unique(@mods, keys %keep);
# Find the current parent group # Find the current parent group
my $oldgroup; my $oldgroup;
if ($in{'old'}) { if ($in{'old'}) {
@@ -80,9 +99,6 @@ if (defined($in{'group'})) {
} }
} }
# Work out group modules
my @mods = split(/\0/, $in{'mod'});
if ($oldgroup) { if ($oldgroup) {
# Remove modules from the old parent group # Remove modules from the old parent group
@mods = grep { &indexof($_, @{$oldgroup->{'modules'}}) < 0 } @mods; @mods = grep { &indexof($_, @{$oldgroup->{'modules'}}) < 0 } @mods;

View File

@@ -103,12 +103,7 @@ if ($in{'temp'}) {
# Find logged-in webmin user # Find logged-in webmin user
my @ulist = &list_users(); my @ulist = &list_users();
my $me; my ($me) = grep { $_->{'name'} eq $base_remote_user } @ulist;
foreach my $u (@ulist) {
if ($u->{'name'} eq $base_remote_user) {
$me = $u;
}
}
# Find the current group # Find the current group
my $oldgroup = $in{'old'} ? &get_users_group($in{'old'}) : undef; my $oldgroup = $in{'old'} ? &get_users_group($in{'old'}) : undef;
@@ -158,7 +153,7 @@ my %mcan = map { $_, 1 } @mcan;
my @mods = split(/\0/, $in{'mod'}); my @mods = split(/\0/, $in{'mod'});
foreach my $m (@mods) { foreach my $m (@mods) {
$mcan{$m} || &error(&text('save_emod', $m)); $mcan{$m} || &error(&text('save_emod', &html_escape($m)));
} }
if ($in{'old'}) { if ($in{'old'}) {
# Add modules that this user already has, but were not # Add modules that this user already has, but were not

View File

@@ -1474,6 +1474,25 @@ subtest 'save_user.cgi create privilege' => sub {
delete_user('admin'); delete_user('admin');
}; };
subtest 'save_user.cgi rejected module escaping' => sub {
_reset_fixture();
_seed_user_acl('admin', { create => 1, mode => 2, mods => 'proc' });
create_user({ name => 'admin', pass => 'x', modules => ['acl', 'proc'] });
# Ordinary and hostile module names must remain text in the error page.
for my $module ('cron', '<script>alert(1)</script>') {
my $r = run_cgi('save_user.cgi', {
name => 'newbie', pass_def => 1, mod => $module,
logouttime_def => 1, minsize_def => 1 });
my $expected = text('save_emod', html_escape($module));
like($r->{out}, qr/\Q$expected\E/, 'rejected module is escaped');
unlike($r->{out}, qr/<script>alert\(1\)<\/script>/,
'module error cannot inject a script');
_clear_caches();
ok(!get_user('newbie'), 'rejected module does not create an account');
}
};
# Unix conversion must not bypass the privileged-name guard in save_user.cgi. # Unix conversion must not bypass the privileged-name guard in save_user.cgi.
# An ACL-capable administrator can still deliberately convert such a user. # An ACL-capable administrator can still deliberately convert such a user.
subtest 'convert.cgi privileged username gate' => sub { subtest 'convert.cgi privileged username gate' => sub {
@@ -1650,6 +1669,125 @@ subtest 'save_group.cgi name validation' => sub {
ok(!$g, 'group with colon-bearing description not created'); ok(!$g, 'group with colon-bearing description not created');
}; };
# Group module limits must reject requests before any membership is changed.
subtest 'save_group.cgi module limits' => sub {
_reset_fixture();
create_user({ name => 'admin', pass => 'x', modules => ['acl', 'proc'] });
create_group({ name => 'parent', members => [], modules => ['proc'] });
_seed_user_acl('admin', { mode => 2, mods => 'proc', groups => 1,
gassign => '*' });
my $r = run_cgi('save_group.cgi', {
name => 'child', desc => 'd', group => 'parent', mod => ['proc', 'cron'] });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
'explicit module limit rejects an unauthorized grant');
_clear_caches();
ok(!get_group('child'), 'rejected group is not created');
is_deeply(get_group('parent')->{members}, [],
'rejected save does not change parent membership');
$r = run_cgi('save_group.cgi', {
name => 'child', desc => 'd', group => 'parent', mod => 'proc' });
is($r->{status}, 0, 'allowed group creation exits successfully');
_clear_caches();
is_deeply(get_group('child')->{modules}, ['proc'], 'allowed module is saved');
is_deeply(get_group('parent')->{members}, ['@child'],
'successful save updates parent membership');
$r = run_cgi('save_group.cgi', {
old => 'child', name => 'child', desc => 'changed', group => '', mod => 'cron' });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
'editing an existing group cannot grant an unauthorized module');
_clear_caches();
is_deeply(get_group('parent')->{members}, ['@child'],
'rejected edit does not detach the existing group');
my $payload = '<script>alert(1)</script>';
$r = run_cgi('save_group.cgi', { name => 'badmod', mod => $payload });
like($r->{out}, qr/&lt;script&gt;/, 'rejected module name is escaped');
unlike($r->{out}, qr/\Q$payload\E/, 'module error cannot inject HTML');
# Mode 1 uses the effective user's modules, including mapped logins.
_seed_user_acl('admin', { mode => 1, groups => 1 });
$r = run_cgi('save_group.cgi', { name => 'ownmods', mod => 'proc' },
user => 'mapped-user', env => { BASE_REMOTE_USER => 'admin' });
_clear_caches();
is_deeply(get_group('ownmods')->{modules}, ['proc'],
'mapped login uses the base user module list');
$r = run_cgi('save_group.cgi', { name => 'denied', mod => 'cron' });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
'mode 1 rejects modules the base user does not hold');
delete_user('admin');
_seed_user_acl('admin', { mode => 1, groups => 1 });
$r = run_cgi('save_group.cgi', { name => 'missing', mod => 'proc' });
like($r->{out}, qr/\Q$text{'edit_egone'}\E/, 'missing user returns a normal error');
unlike($r->{err}, qr/undefined value|compilation errors/,
'missing user does not cause a Perl exception');
_clear_caches();
ok(!get_group('missing'), 'missing user cannot create a group');
# Unrestricted mode does not require the mode-1 user record.
_seed_user_acl('admin', { mode => 0, groups => 1 });
$r = run_cgi('save_group.cgi', { name => 'unrestricted', mod => 'cron' });
_clear_caches();
is_deeply(get_group('unrestricted')->{modules}, ['cron'],
'unrestricted module grants remain supported');
};
# Editing unrelated fields must retain modules outside the admin's grant list.
subtest 'existing group module preservation' => sub {
for my $mode (1, 2, 0) {
_reset_fixture();
create_user({ name => 'admin', pass => 'x', modules => ['acl', 'proc'] });
create_user({ name => 'member', pass => 'x', modules => ['proc', 'cron'] });
create_group({ name => 'existing', members => ['member'],
modules => ['proc', 'cron'], desc => 'before' });
_seed_user_acl('admin', { mode => $mode, mods => 'proc', groups => 1 });
my $r = run_cgi('edit_group.cgi', { group => 'existing' });
my ($checkbox) = $r->{out} =~ /(<input\b[^>]*value="cron"[^>]*>)/;
ok($checkbox && $checkbox =~ /checked/, "mode $mode shows existing cron access");
is($checkbox && $checkbox =~ /disabled/ ? 1 : 0, $mode ? 1 : 0,
"mode $mode disables only restricted modules");
# Disabled controls are omitted by the browser.
$r = run_cgi('save_group.cgi', {
old => 'existing', name => 'existing', desc => 'after', mod => 'proc' });
_clear_caches();
my $group = get_group('existing');
is($group->{desc}, 'after', "mode $mode permits description edits");
my @expected = $mode ? qw(cron proc) : qw(proc);
is_deeply([sort @{$group->{modules}}], \@expected,
"mode $mode preserves restricted modules but allows authorized removal");
is_deeply([sort @{get_user('member')->{modules}}], \@expected,
"mode $mode preserves the correct member access");
next if !$mode;
# An old form can still submit the existing restricted selection.
$r = run_cgi('save_group.cgi', {
old => 'existing', name => 'existing', desc => 'old form',
mod => ['proc', 'cron'] });
_clear_caches();
is(get_group('existing')->{desc}, 'old form',
"mode $mode accepts an unchanged restricted selection");
$r = run_cgi('save_group.cgi', {
old => 'existing', name => 'existing', desc => 'forged',
mod => ['proc', 'cron', 'webmin'] });
like($r->{out}, qr/You cannot grant access to module 'webmin'/,
"mode $mode rejects a new unauthorized module");
_clear_caches();
is(get_group('existing')->{desc}, 'old form',
"mode $mode rejects before writing changes");
$r = run_cgi('save_group.cgi', {
name => 'cloned', clone => 'existing', mod => ['proc', 'cron'] });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
"mode $mode cannot grant restricted modules by cloning");
}
};
subtest 'save_user.cgi self-lockout guard' => sub { subtest 'save_user.cgi self-lockout guard' => sub {
# Editing yourself out of the acl module is refused — otherwise an # Editing yourself out of the acl module is refused — otherwise an
# admin could lock themselves out of Webmin entirely. # admin could lock themselves out of Webmin entirely.

View File

@@ -2,7 +2,7 @@ name=Partition Manager
category=hardware category=hardware
os_support=*-linux os_support=*-linux
desc=Partitions on Local Disks desc=Partitions on Local Disks
depends=mount proc raid depends=mount proc
longdesc=Create and edit partitions on local SCSI, IDE and SATA disks on Linux. longdesc=Create and edit partitions on local SCSI, IDE and SATA disks on Linux.
novserver=1 novserver=1
noopenvz=1 noopenvz=1

View File

@@ -297,7 +297,7 @@ else {
sub print_errors sub print_errors
{ {
my (@errors) = @_; my (@errors) = @_;
&ui_print_header(undef, $module_info{'name'}, ""); &ui_print_header(undef, $module_info{'desc'}, "");
print "<tt>$text{'errors_occured'}</tt><br>"; print "<tt>$text{'errors_occured'}</tt><br>";
print "<ul class=\"err-body\">"; print "<ul class=\"err-body\">";
foreach $error (@errors) { foreach $error (@errors) {

View File

@@ -13,7 +13,7 @@ unless (opendir(DIR, $cwd)) {
} }
else { else {
&ui_print_header( &ui_print_header(
undef, $module_info{'name'}, "", undef, 0, 0, 0, undef, $module_info{'desc'}, "", undef, 0, 0, 0,
"<a href='config.cgi?path=" . &urlize($path) . "<a href='config.cgi?path=" . &urlize($path) .
"' data-config-pagination='$userconfig{'per_page'}'>" . "' data-config-pagination='$userconfig{'per_page'}'>" .
"$text{'module_config'}</a>"); "$text{'module_config'}</a>");

View File

@@ -1,5 +1,4 @@
webmin=1 webmin=1
longdesc=Fast and light File Manager written in Perl. longdesc=Fast and light File Manager written in Perl.
name=File Manager
usermin=1 usermin=1
desc=File Manager desc=File Manager

View File

@@ -1 +0,0 @@
name_ar=مدير الملفات

View File

@@ -1,3 +1,2 @@
longdesc_bg=Бърз и лек файлов мениджър, написан на Perl. longdesc_bg=Бърз и лек файлов мениджър, написан на Perl.
desc_bg=Файлов мениджър desc_bg=Файлов мениджър
name_bg=Файлов мениджър

View File

@@ -1 +0,0 @@
name_ca=Gestor d'arxius

View File

@@ -1,3 +1,2 @@
longdesc_cs=Rychlý a lehký Správce souborů psaný v Perlu. longdesc_cs=Rychlý a lehký Správce souborů psaný v Perlu.
desc_cs=Správce souborů desc_cs=Správce souborů
name_cs=Správce souborů

View File

@@ -1,3 +1,2 @@
desc_da=Filhåndtering desc_da=Filhåndtering
name_da=Filhåndtering
longdesc_da=Hurtig og let File Manager skrevet i Perl. longdesc_da=Hurtig og let File Manager skrevet i Perl.

View File

@@ -1 +0,0 @@
name_de=Dateimanager

View File

@@ -1,3 +1,2 @@
longdesc_el=Γρήγορος και ελαφρύς διαχειριστής αρχείων γραμμένος σε Perl. longdesc_el=Γρήγορος και ελαφρύς διαχειριστής αρχείων γραμμένος σε Perl.
desc_el=Διαχείριση αρχείων desc_el=Διαχείριση αρχείων
name_el=Διαχείριση αρχείων

View File

@@ -1,3 +1,2 @@
desc_es=Administrador de archivos desc_es=Administrador de archivos
name_es=Administrador de archivos
longdesc_es=Administrador de archivos rápido y ligero escrito en Perl. longdesc_es=Administrador de archivos rápido y ligero escrito en Perl.

View File

@@ -1,3 +1,2 @@
name_eu=Fitxategi kudeatzailea
desc_eu=Fitxategi kudeatzailea desc_eu=Fitxategi kudeatzailea
longdesc_eu=Fitxategi kudeatzaile azkarra eta arina Perlen idatzita. longdesc_eu=Fitxategi kudeatzaile azkarra eta arina Perlen idatzita.

View File

@@ -1,3 +1,2 @@
name_fa=مدیر فایل
longdesc_fa=مدیر سریع و سبک فایل در Perl نوشته شده است. longdesc_fa=مدیر سریع و سبک فایل در Perl نوشته شده است.
desc_fa=مدیر فایل desc_fa=مدیر فایل

View File

@@ -1,3 +1,2 @@
name_fi=Tiedostonhallinta
desc_fi=Tiedostonhallinta desc_fi=Tiedostonhallinta
longdesc_fi=Nopea ja kevyt File Manager kirjoitettu Perl. longdesc_fi=Nopea ja kevyt File Manager kirjoitettu Perl.

View File

@@ -0,0 +1 @@
desc_fr=Gestionnaire de Fichiers

View File

@@ -1,3 +1,2 @@
longdesc_fr=Gestionnaire de fichiers rapide et léger écrit en Perl. longdesc_fr=Gestionnaire de fichiers rapide et léger écrit en Perl.
desc_fr=Gestionnaire de fichiers desc_fr=Gestionnaire de fichiers
name_fr=Gestionnaire de fichiers

View File

@@ -1,3 +1,2 @@
desc_hr=Upravitelj datoteka desc_hr=Upravitelj datoteka
name_hr=Upravitelj datoteka
longdesc_hr=Brz i lagan Upravitelj datoteka napisan na Perlu. longdesc_hr=Brz i lagan Upravitelj datoteka napisan na Perlu.

View File

@@ -1,3 +1,2 @@
desc_hu=Fájl kezelő desc_hu=Fájl kezelő
name_hu=Fájl kezelő
longdesc_hu=Gyors és könnyű fájlkezelő, Perl nyelven írva. longdesc_hu=Gyors és könnyű fájlkezelő, Perl nyelven írva.

View File

@@ -1,3 +1,2 @@
desc_it=Gestore file desc_it=Gestore file
name_it=Gestore file
longdesc_it=Gestore file veloce e leggero scritto in Perl. longdesc_it=Gestore file veloce e leggero scritto in Perl.

View File

@@ -1,3 +1,2 @@
name_ja=ファイル管理
desc_ja=ファイル管理 desc_ja=ファイル管理
longdesc_ja=Perlで書かれた高速で軽量のファイルマネージャー。 longdesc_ja=Perlで書かれた高速で軽量のファイルマネージャー。

View File

@@ -1,3 +1,2 @@
desc_ko=파일 관리자 desc_ko=파일 관리자
name_ko=파일 관리자
longdesc_ko=Perl로 작성된 빠르고 가벼운 파일 관리자. longdesc_ko=Perl로 작성된 빠르고 가벼운 파일 관리자.

View File

@@ -1,3 +1,2 @@
desc_ms=Pengurus Fail desc_ms=Pengurus Fail
name_ms=Pengurus Fail
longdesc_ms=Pengurus Fail pantas dan ringan yang ditulis dalam Perl. longdesc_ms=Pengurus Fail pantas dan ringan yang ditulis dalam Perl.

View File

@@ -1,3 +1,2 @@
name_nl=Bestandsbeheer
desc_nl=Bestandsbeheer desc_nl=Bestandsbeheer
longdesc_nl=Snelle en lichte File Manager geschreven in Perl. longdesc_nl=Snelle en lichte File Manager geschreven in Perl.

View File

@@ -1,2 +1 @@
name_no=Filbehandler
longdesc_no=Rask og lett File Manager skrevet i Perl. longdesc_no=Rask og lett File Manager skrevet i Perl.

View File

@@ -1,3 +1,2 @@
desc_pl=Menedżer plików desc_pl=Menedżer plików
longdesc_pl=Szybki i lekki menedżer plików napisany w Perlu. longdesc_pl=Szybki i lekki menedżer plików napisany w Perlu.
name_pl=Menedżer plików

View File

@@ -1,3 +1,2 @@
desc_pt=Gerenciador de arquivos desc_pt=Gerenciador de arquivos
name_pt=Gerenciador de arquivos
longdesc_pt=Gerenciador de arquivos rápido e leve, escrito em Perl. longdesc_pt=Gerenciador de arquivos rápido e leve, escrito em Perl.

View File

@@ -1,3 +1,2 @@
desc_pt_BR=Gerenciador de arquivos desc_pt_BR=Gerenciador de arquivos
name_pt_BR=Gerenciador de arquivos
longdesc_pt_BR=Gerenciador de arquivos rápido e leve, escrito em Perl. longdesc_pt_BR=Gerenciador de arquivos rápido e leve, escrito em Perl.

View File

@@ -1,3 +1,2 @@
longdesc_ru=Быстрый и легкий файловый менеджер, написанный на Perl. longdesc_ru=Быстрый и легкий файловый менеджер, написанный на Perl.
name_ru=Файловый менеджер
desc_ru=Файловый менеджер desc_ru=Файловый менеджер

View File

@@ -1,3 +1,2 @@
desc_sk=Správca súborov desc_sk=Správca súborov
name_sk=Správca súborov
longdesc_sk=Rýchly a ľahký Správca súborov napísaný v jazyku Perl. longdesc_sk=Rýchly a ľahký Správca súborov napísaný v jazyku Perl.

View File

@@ -1,3 +1,2 @@
desc_sv=Filhanterare desc_sv=Filhanterare
name_sv=Filhanterare
longdesc_sv=Snabb och lätt File Manager skriven i Perl. longdesc_sv=Snabb och lätt File Manager skriven i Perl.

View File

@@ -1 +0,0 @@
name_tr=Dosya Yöneticisi

View File

@@ -1,6 +1,4 @@
name_uk=Файловий менеджер
desc_uk=Файловий менеджер desc_uk=Файловий менеджер
longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl. longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl.
name=Файловий менеджер
desc=Файловий менеджер desc=Файловий менеджер
longdesc=Швидкий і легкий менеджер файлів, написаний на Perl. longdesc=Швидкий і легкий менеджер файлів, написаний на Perl.

View File

@@ -1,3 +1,2 @@
name_uk=Файловий менеджер
desc_uk=Файловий менеджер desc_uk=Файловий менеджер
longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl. longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl.

View File

@@ -1,3 +1,2 @@
longdesc_zh=用Perl编写的快捷文件管理器。 longdesc_zh=用Perl编写的快捷文件管理器。
name_zh=文件管理器
desc_zh=文件管理器 desc_zh=文件管理器

View File

@@ -1,3 +1,2 @@
desc_zh_TW=文件管理器 desc_zh_TW=文件管理器
longdesc_zh_TW=用Perl編寫的快捷文件管理器。 longdesc_zh_TW=用Perl編寫的快捷文件管理器。
name_zh_TW=文件管理器

View File

@@ -1319,6 +1319,10 @@ details.ui_hidden_table_start > table {
padding: 8px 14px 10px; padding: 8px 14px 10px;
overflow-x: auto; overflow-x: auto;
} }
/* Open menus must reach beyond the section without adding scrollbars. */
details.ui_hidden_table_start > table:has(.ui_dropdown[open]) {
overflow: visible;
}
details.ui_hidden_table_start > table > tbody { details.ui_hidden_table_start > table > tbody {
display: table; display: table;
width: 100%; width: 100%;

View File

@@ -9,6 +9,7 @@ main_none=Vous n'avez accès à aucun modules Webmin.
main_skill=Niveau de compétence main_skill=Niveau de compétence
main_readonly=(Mode lecture seule) main_readonly=(Mode lecture seule)
main_return=Retourner à $1 main_return=Retourner à $1
main_unused=Modules Inutilisés
link_essl=The Net::Le module SSLeay Perl requis pour créer des connections HTTPS n'est pas installé sur votre système. link_essl=The Net::Le module SSLeay Perl requis pour créer des connections HTTPS n'est pas installé sur votre système.
@@ -20,6 +21,7 @@ category_net=Réseau
category_syslet=Syslets category_syslet=Syslets
category_cluster=Cluster category_cluster=Cluster
category_info=Information category_info=Information
category_=Outils
longcategory_servers=Modules pour les configurations web, email, FTP et autres serveurs longcategory_servers=Modules pour les configurations web, email, FTP et autres serveurs
longcategory_hardware=Modules pour les imprimantes, disques et autres configurations de matériel longcategory_hardware=Modules pour les imprimantes, disques et autres configurations de matériel

View File

@@ -904,6 +904,9 @@ sub device_input
{ {
local (%used, $vg, $pv, $d, $p); local (%used, $vg, $pv, $d, $p);
# Dropdown labels need plain text sizes without theme HTML.
local $main::theme_forbid_nice_size = 1;
# Find partitions that are part of an LVM # Find partitions that are part of an LVM
foreach $vg (&list_volume_groups()) { foreach $vg (&list_volume_groups()) {
foreach $pv (&list_physical_volumes($vg->{'name'})) { foreach $pv (&list_physical_volumes($vg->{'name'})) {
@@ -931,7 +934,8 @@ foreach $d (&fdisk::list_disks_partitions()) {
} }
} }
# Show available RAID devices # Show available RAID devices when the module is installed
if ($has_raid) {
local $conf = &raid::get_raidtab(); local $conf = &raid::get_raidtab();
foreach $c (@$conf) { foreach $c (@$conf) {
next if ($used{$c->{'value'}}); next if ($used{$c->{'value'}});
@@ -939,6 +943,7 @@ foreach $c (@$conf) {
next if (@ds); next if (@ds);
push(@opts, [ $c->{'value'}, &text('pv_raid', $c->{'value'} =~ /md(\d+)$/ ? "$1" : $c->{'value'}) ]); push(@opts, [ $c->{'value'}, &text('pv_raid', $c->{'value'} =~ /md(\d+)$/ ? "$1" : $c->{'value'}) ]);
} }
}
push(@opts, [ '', $text{'pv_other'} ]); push(@opts, [ '', $text{'pv_other'} ]);
return &ui_select("device", $opts[0]->[0], \@opts)." ". return &ui_select("device", $opts[0]->[0], \@opts)." ".

View File

@@ -5,7 +5,7 @@
require './proc-lib.pl'; require './proc-lib.pl';
&ReadParse(); &ReadParse();
&switch_acl_uid(); &switch_acl_uid();
&error_setup(&text('kill_err', $in{signal}, $in{pid})); &error_setup(&text('kill_err', &html_escape($in{signal}), &html_escape($in{pid})));
foreach $s ('KILL', 'TERM', 'HUP', 'STOP', 'CONT') { foreach $s ('KILL', 'TERM', 'HUP', 'STOP', 'CONT') {
$in{'signal'} = $s if ($in{$s}); $in{'signal'} = $s if ($in{$s});
} }

View File

@@ -14,10 +14,10 @@ foreach $s ('KILL', 'TERM', 'HUP', 'STOP', 'CONT') {
@pinfo = &list_processes(@pidlist); @pinfo = &list_processes(@pidlist);
for($i=0; $i<@pidlist; $i++) { for($i=0; $i<@pidlist; $i++) {
$in{"args$i"} = $pinfo[$i]->{'args'}; $in{"args$i"} = $pinfo[$i]->{'args'};
print "$text{'pid'} <tt>$pidlist[$i]</tt> ... \n"; print "$text{'pid'} <tt>".&html_escape($pidlist[$i])."</tt> ... \n";
if (&can_edit_process($pinfo[$i]->{'user'})) { if (&can_edit_process($pinfo[$i]->{'user'})) {
if (&kill_logged($in{signal}, $pidlist[$i])) { if (&kill_logged($in{signal}, $pidlist[$i])) {
print "SIG$in{signal} $text{'kill_sent'}<br>\n"; print "SIG".&html_escape($in{'signal'})." $text{'kill_sent'}<br>\n";
} }
else { else {
print "$!<br>\n"; print "$!<br>\n";
@@ -29,5 +29,6 @@ for($i=0; $i<@pidlist; $i++) {
} }
&webmin_log("kill", undef, undef, \%in); &webmin_log("kill", undef, undef, \%in);
print "<p>\n"; print "<p>\n";
&ui_print_footer("index_search.cgi?$in{'args'}", $text{'search_return'}); &ui_print_footer("index_search.cgi?".&html_escape($in{'args'}),
$text{'search_return'});

View File

@@ -435,6 +435,7 @@ comment_header=Table comment details
comment_msg=Message comment_msg=Message
comment_err=Failed to save comment comment_err=Failed to save comment
comment_enone=No message entered comment_enone=No message entered
comment_ereturn=Invalid message entered
log_create_table=Added entry to the $1 table log_create_table=Added entry to the $1 table
log_modify_table=Modified entry in the $1 table log_modify_table=Modified entry in the $1 table

View File

@@ -59,7 +59,7 @@ if (@table) {
push(@cols, &ui_link( push(@cols, &ui_link(
"editcmt.cgi?table=".&urlize($in{'table'}). "editcmt.cgi?table=".&urlize($in{'table'}).
"&idx=".&urlize($i), "&idx=".&urlize($i),
"<i>".join(" ", @t[1..$#t])."</i>")); "<i>".&html_escape(join(" ",@t[1..$#t]))."</i>"));
@tds = ( "width=5", "colspan=".scalar(@colnames) ); @tds = ( "width=5", "colspan=".scalar(@colnames) );
} }
else { else {
@@ -71,10 +71,11 @@ if (@table) {
if ($j == 0) { if ($j == 0) {
$lnk = &ui_link( $lnk = &ui_link(
"edit.cgi?table=".&urlize($in{'table'}). "edit.cgi?table=".&urlize($in{'table'}).
"&idx=".&urlize($i), $t[$j]); "&idx=".&urlize($i),
&html_escape($t[$j]));
} }
else { else {
$lnk = $t[$j]; $lnk = &html_escape($t[$j]);
} }
push(@cols, $lnk); push(@cols, $lnk);
} }

View File

@@ -15,6 +15,7 @@ if ($in{'delete'}) {
else { else {
# Validate inputs # Validate inputs
$in{'msg'} =~ /\S/ || &error($text{'comment_enone'}); $in{'msg'} =~ /\S/ || &error($text{'comment_enone'});
$in{'msg'} =~ /\r|\0/ && &error($text{'comment_ereturn'});
$line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}"; $line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}";
# Update or add # Update or add

View File

@@ -421,6 +421,7 @@ comment_header=Table comment details
comment_msg=Message comment_msg=Message
comment_err=Failed to save comment comment_err=Failed to save comment
comment_enone=No message entered comment_enone=No message entered
comment_ereturn=Invalid message entered
log_create_table=Added entry to the $1 table log_create_table=Added entry to the $1 table
log_modify_table=Modified entry in the $1 table log_modify_table=Modified entry in the $1 table

View File

@@ -57,8 +57,10 @@ if (@table) {
local @tds; local @tds;
if ($t[0] =~ /\??COMMENT/) { if ($t[0] =~ /\??COMMENT/) {
# Special case - a comment line # Special case - a comment line
push(@cols, "<a href='editcmt.cgi?table=$in{'table'}&". push(@cols, "<a href='editcmt.cgi?table=".
"idx=$i'><i>".join(" ", @t[1..$#t]). &urlize($in{'table'})."&idx=".
&urlize($i)."'><i>".
&html_escape(join(" ", @t[1..$#t])).
"</i></a>" ); "</i></a>" );
@tds = ( "width=5", "colspan=".scalar(@colnames) ); @tds = ( "width=5", "colspan=".scalar(@colnames) );
} }
@@ -69,10 +71,13 @@ if (@table) {
} }
for($j=0; $j<@colnames; $j++) { for($j=0; $j<@colnames; $j++) {
if ($j == 0) { if ($j == 0) {
$lnk = &ui_link("edit.cgi?table=$in{'table'}&idx=$i",$t[$j]); $lnk = &ui_link(
"edit.cgi?table=".&urlize($in{'table'}).
"&idx=".&urlize($i),
&html_escape($t[$j]));
} }
else { else {
$lnk = $t[$j]; $lnk = &html_escape($t[$j]);
} }
push(@cols, $lnk); push(@cols, $lnk);
} }

View File

@@ -15,6 +15,7 @@ if ($in{'delete'}) {
else { else {
# Validate inputs # Validate inputs
$in{'msg'} =~ /\S/ || &error($text{'comment_enone'}); $in{'msg'} =~ /\S/ || &error($text{'comment_enone'});
$in{'msg'} =~ /\r|\0/ && &error($text{'comment_ereturn'});
$line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}"; $line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}";
# Update or add # Update or add

View File

@@ -523,8 +523,8 @@ close(PKG);
while(<PKG>) { while(<PKG>) {
s/\r|\n//g; s/\r|\n//g;
next if (/^(Loaded|updateinfo)/); next if (/^(Loaded|updateinfo)/);
if (/^\S+\s+\S+\s+(\S+?)\-([0-9]\S+)\.([^\.]+)$/) { if (/^\S+\s+(\S*[Ss]ec\S*)\s+(\S+?)\-([0-9]\S+)\.([^\.]+)$/) {
local ($name, $ver) = ($1, $2); local ($name, $ver) = ($2, $3);
if ($done->{$name}) { if ($done->{$name}) {
$done->{$name}->{'source'} ||= 'security'; $done->{$name}->{'source'} ||= 'security';
$done->{$name}->{'security'} = 1; $done->{$name}->{'security'} = 1;

118
t/cert-proc-escaping.t Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/perl
# Exercise CGI escaping without issuing certificates or signaling processes.
use strict;
use warnings;
no warnings qw(once redefine uninitialized);
use Test::More;
use FindBin;
my $root = "$FindBin::Bin/..";
require "$root/web-lib-funcs.pl";
require "$root/ui-lib.pl";
$INC{'./acl-lib.pl'} = $INC{'./proc-lib.pl'} = 1;
*main::ReadParse = *main::switch_acl_uid = *main::webmin_log = sub { };
*main::error_setup = *main::get_miniserv_config = sub { };
*main::error = sub { die $_[0]."\n" };
*main::get_module_variable = sub { return \%main::text };
*main::ui_print_unbuffered_header = sub { };
*main::load_theme_library = sub { };
*main::get_module_name = sub { 'proc' };
*main::get_module_info = sub { return () };
*main::get_webprefix = sub { '' };
for my $file ('lang/en', 'acl/lang/en', 'proc/lang/en') {
open(my $fh, '<', "$root/$file") or die $!;
while (<$fh>) {
$main::text{$1} = $2 if /^(\S+)=(.*)$/;
}
}
# render_cgi(file, inputs) captures the real CGI and UI helpers.
sub render_cgi
{
my ($file, $inputs) = @_;
local %main::in = %$inputs;
my $html = '';
open(my $out, '>', \$html) or die $!;
local *STDOUT = $out;
do "$root/$file";
return ($html, $@);
}
subtest 'certificate success and error output' => sub {
# Stub only certificate I/O and account writes; preserve submitted values.
my ($saved, $openssl_error);
local *main::transname = local *main::tempname = sub { '/fixture/cert' };
local *main::open_tempfile = local *main::print_tempfile =
local *main::close_tempfile = local *main::unlink_file = sub { };
local *main::get_ssleay = sub { 'openssl' };
local *main::backquote_logged = sub {
$? = $openssl_error ? 256 : 0;
return $openssl_error || '';
};
local *main::list_users = sub { return {name => 'fixture'} };
# Stop after the tested account write, before the CGI's delay and restart.
local *main::modify_user = sub { $saved = $_[1]->{cert}; die "fixture done\n" };
local $main::base_remote_user = 'fixture';
my $payload = '<script>alert(1)</script>';
for my $name ('Review User', q{O'Reilly & Partners}, $payload) {
my ($html, $error) = render_cgi('acl/cert_issue.cgi',
{ key => 'fixture-key', commonName => $name });
is($error, "fixture done\n", 'successful issuance reaches account update');
like($html, qr/\Q@{[main::html_escape($name)]}\E/,
'common name appears as text');
unlike($html, qr/\Q$payload\E/, 'common name cannot introduce a script');
like($saved, qr/\Q\/CN=$name\/Email=\E/, 'stored DN retains the original name');
}
$openssl_error = "Invalid subject $payload\n";
my ($html, $error) = render_cgi('acl/cert_issue.cgi',
{ key => 'fixture-key', commonName => 'Review User' });
like($error, qr/<pre>Invalid subject &lt;script&gt;/,
'OpenSSL diagnostics retain preformatted markup and escape input');
unlike($error, qr/\Q$payload\E/, 'OpenSSL errors cannot introduce a script');
};
subtest 'single-process permission error heading' => sub {
my ($heading, $pid);
local *main::error_setup = sub { $heading = $_[0] };
local *main::process_info = sub { $pid = $_[0]; return (user => 'fixture') };
local *main::can_edit_process = sub { 0 };
local *main::kill_logged = sub { die "unexpected signal\n" };
my $payload = '<script>alert(1)</script>';
for my $inputs ({signal => 'TERM', pid => '12345'},
{signal => $payload, pid => $payload}) {
my ($html, $error) = render_cgi('proc/kill_proc.cgi', $inputs);
is($error, "$main::text{'kill_ecannot'}\n", 'signaling is denied');
is($heading, main::text('kill_err',
main::html_escape($inputs->{signal}),
main::html_escape($inputs->{pid})),
'error heading renders submitted values as text');
is($pid, $inputs->{pid}, 'process lookup receives the original PID');
}
};
subtest 'process output and return link' => sub {
my @signals;
local *main::list_processes = sub { return map { +{user => 'fixture'} } @_ };
local *main::can_edit_process = sub { 1 };
local *main::kill_logged = sub { push @signals, [@_]; return 1 };
my $query = 'search=worker%20name&match=1';
my ($html, $error) = render_cgi('proc/kill_proc_list.cgi',
{pidlist => '12345 12346', signal => 'TERM', args => $query});
is($error, '', 'ordinary request renders successfully');
is_deeply(\@signals, [['TERM', '12345'], ['TERM', '12346']],
'signal and PID arguments are unchanged');
$html =~ /href="(index_search\.cgi\?[^"]+)"/;
is(main::html_unescape($1), "index_search.cgi?$query",
'HTML escaping preserves the return query');
my $payload = '<script>alert(1)</script>';
($html, $error) = render_cgi('proc/kill_proc_list.cgi',
{pidlist => $payload, signal => $payload, args => '">'.$payload});
is($error, '', 'hostile values render successfully');
unlike($html, qr/\Q$payload\E/, 'PID, signal and return link cannot inject scripts');
like($html, qr/SIG&lt;script&gt;/, 'signal output is escaped');
like($html, qr/href="index_search.cgi\?&quot;&gt;&lt;script&gt;/,
'return link cannot escape its quoted attribute');
};
done_testing();

View File

@@ -4761,7 +4761,9 @@ between.
=item gap - Gap between items, as a CSS length. =item gap - Gap between items, as a CSS length.
=item class - Additional CSS classes. =item class - Additional CSS classes. Use ui_cluster_small for text fields
and single-select menus beside small action buttons. Fields match the
button height set by the theme, and menus fit their selected text.
=cut =cut
sub ui_cluster sub ui_cluster

View File

@@ -547,6 +547,27 @@ body:has(.ui_page[data-ui-scheme="dark"]) {
.ui_cluster_end { justify-content: flex-end; } .ui_cluster_end { justify-content: flex-end; }
.ui_cluster_between { justify-content: space-between; } .ui_cluster_between { justify-content: space-between; }
/* Fields beside small actions take their height from the button. */
.ui_cluster_small { align-items: stretch; }
.ui_cluster_small > label { align-self: center; line-height: 1; }
.ui_cluster.ui_cluster_small > :is(input.ui_textbox, select.ui_select) {
box-sizing: border-box;
height: auto !important;
min-height: 0 !important;
max-height: none !important;
padding-block: 0 1px !important;
font-size: 0.9em !important;
line-height: 1 !important;
}
/* Keep content-width menus and center the arrow within the compact field. */
.ui_cluster.ui_cluster_small > select.ui_select {
width: auto !important;
min-width: 0 !important;
field-sizing: content;
padding-inline: 4px 18px !important;
background-position: right 3px center !important;
}
@media (max-width: 760px) { @media (max-width: 760px) {
.ui_grid_fixed { grid-template-columns: 1fr; } .ui_grid_fixed { grid-template-columns: 1fr; }
} }
@@ -1270,8 +1291,8 @@ a.ui_list_link:hover { color: var(--ui-accent) !important; }
display: grid; display: grid;
gap: var(--ui-inset); gap: var(--ui-inset);
min-width: 0; min-width: 0;
/* Half the inset above and below, the full inset at the sides */ /* Match vertical padding to the surrounding page gap */
padding: calc(var(--ui-inset) / 2) var(--ui-inset); padding: var(--ui-gap) var(--ui-inset);
border: 1px solid var(--ui-border); border: 1px solid var(--ui-border);
background: var(--ui-surface); background: var(--ui-surface);
} }
@@ -1944,6 +1965,8 @@ a.ui_list_link:hover { color: var(--ui-accent) !important; }
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
} }
.ui_dropdown_desc { .ui_dropdown_desc {
color: var(--ui-fg-muted); color: var(--ui-fg-muted);

View File

@@ -1 +1 @@
2.670 2.671