Skip to content

Commit bdb8cff

Browse files
committed
Fix a vulnerability in Statistics.pm and the macro PGstatisticsmacros.pl.
This vulnerability allows a problem to write to any location or overwrite any file that the server has write permission to. The problem is that the `write_array_to_CSV` method of the `Statistics.pm` module takes a file name as its first argument and writes to that file without question. To fix this the method instead uses the new `WeBWorK::PG::IO::saveDataToFile` method which refuses to write anything outside of the html temporary directory. Note that the `make_csv_alias` method of the `Statistics.pm` module was dropped because it uses an invalid approach to creating a unique file name and is not needed. So there is no need for the `Statistics` package to even be an object. Now there is just the method `write_array_to_CSV`. Previously the `insertDataLink` method of `PGstatisticsmacros.pl` accepted the `$PG` object for its first argument. That first argument is still accepted, but is dropped if given, and there is no need to pass that argument anymore. The macro has direct access to the `$PG` variable so it is not a needed parameter. Also, a new optional last argument to the `insertDataLink` is accepted. If provide this argument must be a reference to a hash, and the key/value pairs in that hash will be added as attributes to the HTML `a` tak that is returned. If this parameter is not provided or it is but does not contain a `download` attribute, then a default `download` attribute with value `data.csv` will be added. Note that the `insertDataLink` method is not used by any problem in the OPL or Contrib. Furthermore, nothing in the `Statistics.pm` module is directly used by any problem in OPL or Contrib. Backwards compatibility is maintained for the `insertDataLink` method for anyone that may have been using it, but not for the `Statistics.pm` module. It was not intended that the module be used directly anyway. This fixes the second of the three ways that I know of for a problem to directly write to anywhere that the server has permission to do so.
1 parent e59dae7 commit bdb8cff

File tree

2 files changed

+56
-123
lines changed

2 files changed

+56
-123
lines changed

lib/Statistics.pm

+18-91
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
################################################################################
32
# WeBWorK Online Homework Delivery System
43
# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork
@@ -16,108 +15,36 @@
1615

1716
package Statistics;
1817

19-
#use strict;
20-
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
21-
require Exporter;
22-
@ISA = qw(Exporter);
23-
@EXPORT_OK = qw( );
24-
@EXPORT = qw( );
25-
$VERSION = '0.01';
26-
27-
# ##########################################
28-
# Initialize the class
29-
# Expected argument: a ref to a valid PGcore object
30-
#
31-
sub new {
32-
my $class = shift;
33-
my $self = { PG => shift };
34-
35-
bless $self, $class;
36-
37-
}
38-
39-
# ##########################################
40-
# Create an alias to a csv file.
41-
# Return the url that can be used in a browser
42-
# to access the file.
43-
#
44-
sub make_csv_alias {
45-
46-
my $self = shift;
47-
my $studentLogin = shift;
48-
my $problemSeed = shift;
49-
my $setname = shift;
50-
my $prob = shift;
18+
use strict;
19+
use warnings;
5120

52-
# Clean the student login string to make it appropriate for a file name.
53-
$studentLogin =~ s/Q/QQ/g;
54-
$studentLogin =~ s/\./-Q-/g;
55-
$studentLogin =~ s/\,/-Q-/g;
56-
$studentLogin =~ s/\@/-Q-/g;
21+
require WeBWorK::PG::IO;
5722

58-
# Define the file name, clean it up and convert to a url.
59-
my $filePath = "data/$studentLogin-$problemSeed-set" . $setName . "prob$prob.html";
60-
$filePath = $self->{PG}->surePathToTmpFile($filePath);
61-
my $url = $self->{PG}->{PG_alias}->make_alias($filePath);
62-
63-
# Remove the .html off the end and replace it with a .csv
64-
$filePath =~ s/\.html$/.csv/;
65-
$url =~ s/\.html$/.csv/;
66-
67-
($filePath, $url);
68-
}
69-
70-
# ##########################################
7123
# Write the given data to a csv file.
72-
# Return the URL if the file is written,
73-
# an empty string otherwise
74-
#
7524
sub write_array_to_CSV {
76-
my $self = shift;
77-
my $fileName = shift;
78-
my @dataRefs = @_;
25+
my ($fileName, @dataRefs) = @_;
26+
27+
die 'No data set was provided.' unless @dataRefs && ref $dataRefs[0] eq 'ARRAY';
7928

8029
# Make sure all of the data sets have the same number of elements
81-
my $numberDataPoints = "nd";
82-
my $data;
83-
foreach $data (@dataRefs) {
84-
my @dataArray = @{$data};
85-
if ($numberDataPoints eq "nd") {
86-
$numberDataPoints = $#dataArray;
87-
} elsif ($numberDataPoints != $#dataArray) {
88-
die("$0", "The number of elements in the data sets are not all the same. No data set written to file.");
89-
return;
90-
}
30+
my $numberDataPoints = $#{ $dataRefs[0] };
31+
for my $data (@dataRefs) {
32+
die 'The number of elements in the data sets are not all the same. No data set written to file.'
33+
if $numberDataPoints != $#$data;
9134
}
9235

93-
# Open the file
94-
local (*OUTPUT); # create local file handle so it won't overwrite other open files.
95-
open(OUTPUT, ">$fileName") || warn("$0", "Can't open $fileName<BR>", "");
96-
chmod(0777, $filePath);
97-
98-
# write the header to the first row.
99-
my $header = "";
100-
foreach $data (@dataRefs) {
101-
$header .= pop(@{$data}) . ",";
102-
}
103-
$header =~ s/,$//;
104-
print OUTPUT ($header . "\n") || warn("$0", "Can't print data file to $fileName<BR>", "");
36+
# Add the header to the first row of the output.
37+
my $output = join(',', map { $_->[-1] } @dataRefs) . "\n";
10538

106-
# Go through each data point and write it out.
107-
my $lupe;
108-
for ($lupe = 0; $lupe < $numberDataPoints; ++$lupe) {
109-
my $line = "";
110-
foreach $data (@dataRefs) {
111-
my @dataSet = @{$data};
112-
$line .= $dataSet[$lupe] . ",";
113-
}
114-
$line =~ s/,$//;
115-
print OUTPUT ($line, "\n");
39+
# Add each data point as another row in the output.
40+
for my $i (0 .. $numberDataPoints - 1) {
41+
$output .= join(',', map { $_->[$i] } @dataRefs) . "\n";
11642
}
11743

118-
# Close it up and move on.
119-
close(OUTPUT) || warn("$0", "Can't close $filePath<BR>", "");
44+
# Write the output to disk.
45+
WeBWorK::PG::IO::saveDataToFile($output, $fileName);
12046

47+
return;
12148
}
12249

12350
1;

macros/math/PGstatisticsmacros.pl

+38-32
Original file line numberDiff line numberDiff line change
@@ -787,54 +787,60 @@ sub two_sample_t_test {
787787
($t, $df, $p);
788788
}
789789

790-
=head2 Create a data file and make a link to it.
790+
=head2 insertDataLink
791791
792-
=pod
792+
Create a CSV data file and make a link to it.
793793
794-
Usage: insertDataLink($PG,linkText,@dataRefs)
794+
Usage: C<insertDataLink($linkText, @dataRefs, $linkAttributes)>
795795
796-
Writes the given data to a file and creates a link to the data file. The string headerTitle is the label used in the anchor link.
797-
$PG is a ref to an instance of a PGcore object. (Generally just use $PG in a problem)
798-
linkText is the text to appear in the anchor/link.
799-
@dataRefs is a list of references. Each reference is assumed to be ref to an array.
800-
All of the arrays must have the same length.
801-
The last entry in the array is assumed to be the label to use in the first row of the csv file.
796+
Writes the given data to a CSV file and returns a link to the file.
802797
803-
Usage:
804-
# Generate random data
805-
@data1 = urand(10.0,2.0,10,2);
806-
@data2 = urand(12.0,2.0,10,2);
807-
@data3 = urand(14.0,4.0,10,2);
808-
@data4 = exprand(0.1,10,2);
798+
C<$linkText> is the text to appear in the anchor/link.
809799
810-
# Append the labels for each data set
811-
push(@data1,"w");
812-
push(@data2,"x");
813-
push(@data3,"y");
814-
push(@data4,"z");
800+
C<@dataRefs> is a list of references. Each reference is assumed to be ref to an
801+
array. All of the arrays must have the same length. The last entry in the
802+
array is assumed to be the label to use in the first row of the csv file.
815803
816-
BEGIN_TEXT
804+
C<linkAttributes> is optional. If provided, this must be a reference to a hash
805+
containing additional attributes to add to the HTML C<a> tag. If this parameter
806+
is not provided or it is provided and a C<download> attributed is not specified
807+
in the hash, then a default C<download> attribute will be added with the value
808+
"data.csv".
817809
818-
blah blah
810+
Usage:
819811
820-
$BR Data: \{ insertDataLink($PG,"the data",(~~@data1,~~@data2,~~@data3,~~@data4)); \} $BR
812+
# Generate random data with labels.
813+
$data1 = [ urand(10.0, 2.0, 10, 2), 'w' ];
814+
$data2 = [ urand(12.0, 2.0, 10, 2), 'x' ];
815+
$data3 = [ urand(14.0, 4.0, 10, 2), 'y' ];
816+
$data4 = [ exprand(0.1, 10, 2), 'z' ];
821817
818+
BEGIN_PGML
819+
Data: [@ insertDataLink(
820+
'the data', $data1, $data2, $data3, $data4,
821+
{ download => 'problem-dataset.csv' }
822+
) @]*
823+
END_PGML
822824
823825
=cut
824826

825827
sub insertDataLink {
826-
my $PG = shift;
827-
my $linkText = shift;
828-
my @dataRefs = @_;
829-
my $stat = Statistics->new($PG);
828+
my @args = @_;
829+
830+
# If the $PG object was given then just drop it. This is for backwards compatibility.
831+
shift @args if ref($args[0]) eq 'PGcore';
832+
833+
my ($linkText, @dataRefs) = @args;
834+
835+
my $linkAttributes = {};
836+
$linkAttributes = pop @dataRefs if ref $dataRefs[-1] eq 'HASH';
837+
$linkAttributes->{download} = 'data.csv' unless $linkAttributes->{download};
830838

831-
# Create a file name and get the url as well.
832-
my ($fileName, $url) = $stat->make_csv_alias($main::studentLogin, $main::problemSeed, $setName, $main::probNum);
839+
my $filePath = $PG->surePathToTmpFile('data/' . $PG->getUniqueName('csv') . '.csv');
833840

834-
# Now write the data
835-
$stat->write_array_to_CSV($fileName, @dataRefs);
841+
Statistics::write_array_to_CSV($filePath, @dataRefs);
836842

837-
"<a href=\"$url\">$linkText</a>";
843+
return main::tag('a', href => main::alias($filePath), %$linkAttributes, $linkText);
838844
}
839845

840846
=head2 Five Point Summary function

0 commit comments

Comments
 (0)