Skip to content

Commit eebf54d

Browse files
committed
Harden system calls
1 parent e53f566 commit eebf54d

File tree

3 files changed

+61
-19
lines changed

3 files changed

+61
-19
lines changed

lib/LaTeXImage.pm

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,18 @@ sub draw {
271271

272272
sub use_svgMethod {
273273
my ($self, $working_dir) = @_;
274-
if ($self->svgMethod eq 'dvisvgm') {
275-
system WeBWorK::PG::IO::externalCommand('dvisvgm')
276-
. " $working_dir/image.dvi --no-fonts --output=$working_dir/image.svg > /dev/null 2>&1";
274+
275+
# Validate svgMethod against known SVG converters to prevent command injection.
276+
my $method = $self->svgMethod;
277+
if ($method eq 'dvisvgm') {
278+
my $cmd = WeBWorK::PG::IO::externalCommand('dvisvgm');
279+
system {$cmd} $cmd, "$working_dir/image.dvi", '--no-fonts', "--output=$working_dir/image.svg";
280+
} elsif ($method eq 'pdf2svg') {
281+
my $cmd = WeBWorK::PG::IO::externalCommand('pdf2svg');
282+
system {$cmd} $cmd, "$working_dir/image.pdf", "$working_dir/image.svg";
277283
} else {
278-
system WeBWorK::PG::IO::externalCommand($self->svgMethod)
279-
. " $working_dir/image.pdf $working_dir/image.svg > /dev/null 2>&1";
284+
warn "Unknown svgMethod '$method'. Must be 'dvisvgm' or 'pdf2svg'.";
285+
return;
280286
}
281287
warn "Failed to generate svg file." unless -r "$working_dir/image.svg";
282288

@@ -285,11 +291,24 @@ sub use_svgMethod {
285291

286292
sub use_convert {
287293
my ($self, $working_dir, $ext) = @_;
288-
system WeBWorK::PG::IO::externalCommand('convert')
289-
. join('', map { " -$_ " . $self->convertOptions->{input}->{$_} } (keys %{ $self->convertOptions->{input} }))
290-
. " $working_dir/image.pdf"
291-
. join('', map { " -$_ " . $self->convertOptions->{output}->{$_} } (keys %{ $self->convertOptions->{output} }))
292-
. " $working_dir/image.$ext > /dev/null 2>&1";
294+
295+
# Validate convertOptions keys and values to prevent shell injection.
296+
# Only alphanumeric option names and simple values are permitted.
297+
my @args;
298+
for my $phase (qw(input output)) {
299+
my $opts = $self->convertOptions->{$phase} // {};
300+
for my $key (keys %$opts) {
301+
warn("Invalid convert option name: $key"), next unless $key =~ /^[a-zA-Z][a-zA-Z0-9\-]*$/;
302+
my $val = $opts->{$key};
303+
warn("Invalid convert option value for $key"), next unless defined $val && $val =~ /^[a-zA-Z0-9.\-+:x%]+$/;
304+
push @args, "-$key", $val;
305+
}
306+
push @args, "$working_dir/image.pdf" if $phase eq 'input';
307+
}
308+
push @args, "$working_dir/image.$ext";
309+
310+
my $convert = WeBWorK::PG::IO::externalCommand('convert');
311+
system {$convert} $convert, @args;
293312
warn "Failed to generate $ext file." unless -r "$working_dir/image.$ext";
294313

295314
return;

lib/PGalias.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ sub convert_file_to_png_for_tex {
270270
$self->debug_message('convert filePath ', $sourceFilePath, "\n");
271271

272272
my $conversion_command = WeBWorK::PG::IO::externalCommand('convert');
273-
my $returnCode = system "$conversion_command '${sourceFilePath}[0]' $targetFilePath";
273+
my $returnCode = system {$conversion_command} $conversion_command, "${sourceFilePath}[0]", $targetFilePath;
274274
if ($returnCode || !-e $targetFilePath) {
275275
$resource_object->warning_message(
276276
qq{Failed to convert "$sourceFilePath" to "$targetFilePath" using "$conversion_command": $!});

lib/WeBWorK/PG/IO.pm

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,40 @@ sub externalCommand {
316316
# Isolate the call to the sage server in case we have to jazz it up.
317317
sub query_sage_server {
318318
my ($python, $url, $accepted_tos, $setSeed, $webworkfunc, $debug, $curlCommand) = @_;
319-
my $sagecall =
320-
qq{$curlCommand -i -k -sS -L }
321-
. qq{--data-urlencode "accepted_tos=${accepted_tos}" }
322-
. qq{--data-urlencode 'user_expressions={"WEBWORK":"_webwork_safe_json(WEBWORK)"}' }
323-
. qq{--data-urlencode "code=${setSeed}${webworkfunc}$python" $url};
324319

325-
my $output = `$sagecall`;
320+
# Validate url to prevent shell injection — must look like a URL.
321+
if ($url && $url !~ m{^https?://[^\s;|&`\$]+$}) {
322+
warn "query_sage_server: invalid url rejected: $url";
323+
return;
324+
}
325+
326+
# Use system LIST form via open-pipe to capture output without shell interpolation.
327+
my @cmd = (
328+
$curlCommand, '-i',
329+
'-k', '-sS',
330+
'-L', '--data-urlencode',
331+
"accepted_tos=$accepted_tos", '--data-urlencode',
332+
'user_expressions={"WEBWORK":"_webwork_safe_json(WEBWORK)"}', '--data-urlencode',
333+
"code=${setSeed}${webworkfunc}$python", $url // (),
334+
);
335+
326336
if ($debug) {
327337
warn "debug is turned on in IO.pm. ";
328-
warn "\n\nIO::query_sage_server(): SAGE CALL: ", $sagecall, "\n\n";
329-
warn "\n\nRETURN from sage call \n", $output, "\n\n";
338+
warn "\n\nIO::query_sage_server(): SAGE CMD: ", join(' ', @cmd), "\n\n";
339+
}
340+
341+
my $output = '';
342+
if (open(my $pipe, '-|', @cmd)) {
343+
local $/;
344+
$output = <$pipe>;
345+
close $pipe;
346+
} else {
347+
warn "query_sage_server: failed to execute curl: $!";
348+
return;
349+
}
350+
351+
if ($debug) {
352+
warn "\n\nRETURN from sage call \n", $output, "\n\n";
330353
warn "\n\n END SAGE CALL";
331354
}
332355

0 commit comments

Comments
 (0)