Skip to content

Commit

Permalink
printf: remove eval (#526)
Browse files Browse the repository at this point in the history
* Rewrite printf command to not use eval()
* Format string is parsed into a list of literal substrings and format specifiers
* The print loop executes at least once; missing argument are implied as 0 for numbers and '' for strings
* Remove RESTRICTIONS section from pod now that we check input and don't allow arbitrary things into eval()
* Previous version could be exploited like this: perl printf '";;CORE::dump();";dumpassaasdasd'
Aborted

* test1: perl printf 'hey%d' --> implicit argument of zero, "hey0"
* test2: perl printf 'hey%d' 1 --> explicit argument, "hey1"
* test3: perl printf 'hey%d' 1 2 3 --> format string applied 3 times, "hey1hey2hey3"
* test4: perl printf '%20s:%-20s' pri ntf --> length specifier with string, "                 pri:ntf                 "
* test5: perl printf --> error, at least the format string is required
* test6: perl printf "\tX\r\n" | xxd --> c-escapes produce hex output: 0958 0d0a
* test7: perl printf '' --> empty format string is not an error; no output
  • Loading branch information
mknos committed Apr 2, 2024
1 parent bc20fbb commit 5e260f0
Showing 1 changed file with 58 additions and 14 deletions.
72 changes: 58 additions & 14 deletions bin/printf
Expand Up @@ -28,17 +28,67 @@ unless (@ARGV) {
exit EX_FAILURE;
}

my @fmt;
my $format = shift;
$format =~ s/\\v/\x0b/g; # escape \v not available in printf()
$format =~ s/\%c/\%\.1s/g; # standard printf: %c == 1st char

my @ints = map { m/\A0x/i ? hex : int } @ARGV;
eval qq(printf "$format", \@ints) or do {
warn "$Program: $@\n";
exit EX_FAILURE;
};
exit EX_SUCCESS unless (length $format);
parse_fmt();
do {
foreach my $part (@fmt) {
if ($part->[0] eq 'str') {
print escape_str($part->[1]);
} elsif ($part->[0] eq 'ifmt') {
my $fmt = $part->[1];
my $arg = shift;
$arg = 0 unless defined $arg;
if ($arg =~ m/\A0x/i) {
$arg = hex $arg;
}
printf $fmt, $arg;
} elsif ($part->[0] eq 'sfmt') {
my $fmt = $part->[1];
my $arg = shift;
$arg = '' unless defined $arg;
printf $fmt, $arg;
} else {
die "internal error";
}
}
} while (@ARGV);
exit EX_SUCCESS;

sub parse_fmt {
my $f = $format;
$f =~ s/\%c/\%\.1s/g; # standard printf: %c == 1st char

while (length $f) {
if ($f =~ s/\A([^%]+)//) {
push @fmt, [ 'str', $1 ];
} elsif ($f =~ s/\A\%\%//) {
push @fmt, [ 'str', '%%' ];
} elsif ($f =~ s/\A(\%[0-9\.\-]*s)//) {
push @fmt, [ 'sfmt', $1 ];
} elsif ($f =~ s/\A(\%[0-9\.\-]*[diouXx])//) {
push @fmt, [ 'ifmt', $1 ];
} elsif ($f =~ s/\A(\%[0-9\.\-]*[a-zA-Z])//) {
push @fmt, [ 'str', $1 ]; # unsupported
} else {
die "internal error";
}
}
}

sub escape_str {
my $str = shift;
$str =~ s/\\a/\a/g;
$str =~ s/\\b/\b/g;
$str =~ s/\\f/\f/g;
$str =~ s/\\n/\n/g;
$str =~ s/\\r/\r/g;
$str =~ s/\\t/\t/g;
$str =~ s/\\v/\x0b/g;
return $str;
}

__END__
=head1 NAME
Expand All @@ -56,12 +106,6 @@ how to print the remaining arguments. Unlike the standard
printf(1) command, this one uses the Perl version.
See L<perlfunc/sprintf> for details.
=head1 RESTRICTIONS
This command should not be used in setuid programs as it does not run
untaint its argments and will trigger errors like C<Insecure dependency
in eval while running setuid at /opt/ppt/bin/printf line 16.>
=head1 SEE ALSO
printf(3), L<perlfunc/sprintf>
Expand Down

0 comments on commit 5e260f0

Please sign in to comment.