btrbk: code cleanup: check_file() and check_url() do all the sanitize parts

pull/88/head
Axel Burri 2016-04-25 14:23:15 +02:00
parent 191284cd43
commit 357b72bd3f
1 changed files with 42 additions and 35 deletions

77
btrbk
View File

@ -60,6 +60,7 @@ my @config_src = ("/etc/btrbk.conf", "/etc/btrbk/btrbk.conf");
my $ip_addr_match = qr/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/; my $ip_addr_match = qr/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/;
my $host_name_match = qr/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/; my $host_name_match = qr/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/;
my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs> my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the subvolume layout: <https://help.ubuntu.com/community/btrfs>
my $glob_match = qr/[0-9a-zA-Z_@\+\-\.\/\*]+/; # file_match plus '*'
my $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/; my $ssh_prefix_match = qr/ssh:\/\/($ip_addr_match|$host_name_match)/;
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2})((?<ss>[0-9]{2})(?<zz>(Z|[+-][0-9]{4})))?)?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]" my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2})((?<ss>[0-9]{2})(?<zz>(Z|[+-][0-9]{4})))?)?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]"
@ -606,8 +607,9 @@ sub btrfs_subvolume_show($)
my $real_path; my $real_path;
if($ret =~ /^($file_match)/) { if($ret =~ /^($file_match)/) {
$real_path = $1; $real_path = $1;
$real_path = check_file($real_path, { absolute => 1 });
return undef unless(defined($real_path));
DEBUG "Real path for subvolume \"$vol->{PRINT}\" is: $real_path" if($real_path ne $path); DEBUG "Real path for subvolume \"$vol->{PRINT}\" is: $real_path" if($real_path ne $path);
return undef unless(check_file($real_path, { absolute => 1 }));
$realpath_cache{$vol->{URL}} = $real_path if($real_path ne $path); $realpath_cache{$vol->{URL}} = $real_path if($real_path ne $path);
} }
else { else {
@ -2077,30 +2079,25 @@ sub check_file($$;$$)
my $key = shift; # only for error text my $key = shift; # only for error text
my $config_file = shift; # only for error text my $config_file = shift; # only for error text
my $ckfile = $file; my $match = $file_match;
$ckfile =~ s/\*/_/g if($accept->{wildcards}); $match = $glob_match if($accept->{wildcards});
if($accept->{ssh} && ($file =~ /^ssh:\/\//)) { if($file =~ /^($match)$/) {
unless($ckfile =~ /^$ssh_prefix_match\/$file_match$/) { $file = $1;
ERROR "Ambiguous ssh url for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file);
return undef;
}
}
elsif($ckfile =~ /^$file_match$/) {
if($accept->{absolute}) { if($accept->{absolute}) {
unless($ckfile =~ /^\//) { unless($file =~ /^\//) {
ERROR "Only absolute files allowed for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file); ERROR "Only absolute files allowed for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file);
return undef; return undef;
} }
} }
elsif($accept->{relative}) { elsif($accept->{relative}) {
if($ckfile =~ /^\//) { if($file =~ /^\//) {
ERROR "Only relative files allowed for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file); ERROR "Only relative files allowed for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file);
return undef; return undef;
} }
} }
elsif($accept->{name_only}) { elsif($accept->{name_only}) {
if($ckfile =~ /\//) { if($file =~ /\//) {
ERROR "Option \"$key\" is not a valid file name in \"$config_file\" line $.: $file" if($key && $config_file); ERROR "Option \"$key\" is not a valid file name in \"$config_file\" line $.: $file" if($key && $config_file);
return undef; return undef;
} }
@ -2118,7 +2115,21 @@ sub check_file($$;$$)
ERROR "Illegal directory traversal for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file); ERROR "Illegal directory traversal for option \"$key\" in \"$config_file\" line $.: $file" if($key && $config_file);
return undef; return undef;
} }
return 1; $file =~ s/\/+/\//g; # sanitize multiple slash
$file =~ s/\/\.\//\//g; # sanitize "/./" -> "/"
$file =~ s/\/$// unless($file eq '/'); # remove trailing slash
return $file;
}
sub check_url($;$$)
{
my $url = shift // die;
my $key = shift; # only for error text
my $config_file = shift; # only for error text
my $url_prefix = "";
$url_prefix = $1 if($url =~ s/^($ssh_prefix_match)\//\//);
return ( $url_prefix, check_file($url, { absolute => 1 }, $key, $config_file) );
} }
@ -2251,11 +2262,10 @@ sub append_config_option($$$$;$)
elsif($opt->{accept_file}) elsif($opt->{accept_file})
{ {
# be very strict about file options, for security sake # be very strict about file options, for security sake
return undef unless(check_file($value, $opt->{accept_file}, $key, $config_file)); $value = check_file($value, $opt->{accept_file}, $key, $config_file);
return undef unless(defined($value));
TRACE "option \"$key=$value\" is a valid file, accepted"; TRACE "option \"$key=$value\" is a valid file, accepted";
$value =~ s/\/+$//; # remove trailing slash
$value =~ s/^\/+/\//; # sanitize leading slash
$value = "no" if($value eq "."); # maps to undef later $value = "no" if($value eq "."); # maps to undef later
} }
elsif($opt->{accept_regexp}) { elsif($opt->{accept_regexp}) {
@ -2350,15 +2360,14 @@ sub parse_config_line($$$$$)
TRACE "config: context forced to: $cur->{CONTEXT}"; TRACE "config: context forced to: $cur->{CONTEXT}";
# be very strict about file options, for security sake # be very strict about file options, for security sake
return undef unless(check_file($value, { absolute => 1, ssh => 1 }, $key, $file)); my ($url_prefix, $path) = check_url($value, $key, $file);
$value =~ s/\/+$// unless($value =~ /^\/+$/); # remove trailing slash return undef unless(defined($path));
$value =~ s/^\/+/\//; # sanitize leading slash TRACE "config: adding volume \"$url_prefix$path\" to root context";
TRACE "config: adding volume \"$value\" to root context";
die unless($cur->{CONTEXT} eq "root"); die unless($cur->{CONTEXT} eq "root");
my $volume = { CONTEXT => "volume", my $volume = { CONTEXT => "volume",
PARENT => $cur, PARENT => $cur,
SUBSECTION => [], SUBSECTION => [],
url => $value, url => $url_prefix . $path,
}; };
push(@{$cur->{SUBSECTION}}, $volume); push(@{$cur->{SUBSECTION}}, $volume);
$cur = $volume; $cur = $volume;
@ -2374,19 +2383,18 @@ sub parse_config_line($$$$$)
TRACE "config: context changed to: $cur->{CONTEXT}"; TRACE "config: context changed to: $cur->{CONTEXT}";
} }
# be very strict about file options, for security sake # be very strict about file options, for security sake
return undef unless(check_file($value, { relative => 1, wildcards => 1 }, $key, $file)); my $rel_path = check_file($value, { relative => 1, wildcards => 1 }, $key, $file);
$value =~ s/\/+$//; # remove trailing slash return undef unless(defined($rel_path));
$value =~ s/^\/+//; # remove leading slash
TRACE "config: adding subvolume \"$value\" to volume context: $cur->{url}"; TRACE "config: adding subvolume \"$rel_path\" to volume context: $cur->{url}";
my $snapshot_name = $value; my $snapshot_name = $rel_path;
$snapshot_name =~ s/^.*\///; # snapshot_name defaults to subvolume name $snapshot_name =~ s/^.*\///; # snapshot_name defaults to subvolume name
die unless($cur->{CONTEXT} eq "volume"); die unless($cur->{CONTEXT} eq "volume");
my $subvolume = { CONTEXT => "subvolume", my $subvolume = { CONTEXT => "subvolume",
PARENT => $cur, PARENT => $cur,
# SUBSECTION => [], # handled by target propagation # SUBSECTION => [], # handled by target propagation
rel_path => $value, rel_path => $rel_path,
url => $cur->{url} . '/' . $value, url => $cur->{url} . '/' . $rel_path,
snapshot_name => $snapshot_name, snapshot_name => $snapshot_name,
}; };
$subvolume->{GLOB_CONTEXT} = 1 if($value =~ /\*/); $subvolume->{GLOB_CONTEXT} = 1 if($value =~ /\*/);
@ -2401,21 +2409,20 @@ sub parse_config_line($$$$$)
} }
if($value =~ /^(\S+)\s+(\S+)$/) if($value =~ /^(\S+)\s+(\S+)$/)
{ {
my ($target_type, $droot) = ($1, $2); my ($target_type, $url) = ($1, $2);
unless(grep(/^\Q$target_type\E$/, @config_target_types)) { unless(grep(/^\Q$target_type\E$/, @config_target_types)) {
ERROR "Unknown target type \"$target_type\" in \"$file\" line $."; ERROR "Unknown target type \"$target_type\" in \"$file\" line $.";
return undef; return undef;
} }
# be very strict about file options, for security sake # be very strict about file options, for security sake
return undef unless(check_file($droot, { absolute => 1, ssh => 1 }, $key, $file)); my ($url_prefix, $path) = check_url($url, $key, $file);
return undef unless(defined($path));
$droot =~ s/\/+$// unless($droot =~ /^\/+$/); # remove trailing slash TRACE "config: adding target \"$url_prefix$path\" (type=$target_type) to $cur->{CONTEXT} context" . ($cur->{url} ? ": $cur->{url}" : "");
$droot =~ s/^\/+/\//; # sanitize leading slash
TRACE "config: adding target \"$droot\" (type=$target_type) to $cur->{CONTEXT} context" . ($cur->{url} ? ": $cur->{url}" : "");
my $target = { CONTEXT => "target", my $target = { CONTEXT => "target",
PARENT => $cur, PARENT => $cur,
target_type => $target_type, target_type => $target_type,
url => $droot, url => $url_prefix . $path,
}; };
# NOTE: target sections are propagated to the apropriate SUBSECTION in _config_propagate_target() # NOTE: target sections are propagated to the apropriate SUBSECTION in _config_propagate_target()
$cur->{TARGET} //= []; $cur->{TARGET} //= [];