From 0c28eaefd6f203448dd1a26d02389c2fc2c2de62 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 2 Apr 2025 13:00:28 -0500 Subject: [PATCH] Sanitize the courseID from either the URL path or the request parameters. This ensures that only the allowed characters for a courseID are allowed in the URL path. This restriction occurs at the Mojolicious router level. This means that a request like `https://server.edu/webwork2/courseID%22%3E%3Cscript%3Ealert('hello')%3C/script%3E` is just sent to the vanilla "Page not found" page. Note that the URL above (or one like it) comes from a security scan that someone ran, and that this is NOT a security vulnerability. However, it is not handled the best. Currently the above URL results in numerous warnings about the use of an uninitialized value in string concatenation, and then eventually an exception because of a missing database table. None of that is harmful and a URL like that shown can not be manipulated to do anything harmful, but the warnings and exception can be cleaned up. That is dealt with by allowing routes to specify restrictive placeholders that are passed to the `any` or `under` method of the `Mojolicious::Routes::Route` object. For the courseID the restriction is that that portion of the URL must match `qr/[\w-]*/` which is the same restriction used for the courseID when creating a course. I also changed the problemID to use the built in `:num` placeholder type which is equivalent to what was being done before (i.e., `qr/\d+\`). By the way you can run `./bin/webwork2 routes -v` to see all webwork2 routes and the regular expressions that are used to match them. Furthermore, since the courseID can be specified in a request parameter (via the RPC endpoints), the `WeBWorK::CourseEnvironment` chops off any single quotes and everything after them that occur in the passed in `courseName` in the `$seedVars`. The problem is that when the seed variables are `reval`ed into the course environment safe compartment with `$safe->reval("\$$var = '$val';")`, any single quotes in `courseName` end the first single quote in that statement causing a syntax error. The exception from that is ignored because the errors from that `reval` are not caught, but it results in the `courseName` in the course environment being undefined. That is what causes all of the unininitialized warnings mentioned above, and furthermore the exception from the missing database table (because the login page is loaded and tries to look up guest users for the course, but the user table doesn't exist for this undefined course name). This is all the result of investigating the suspected vulnerability posted about in https://webwork.maa.org/moodle/mod/forum/discuss.php?d=8686. The information was emailed to thewebworkproject@gmail.com, and relayed to @dlgin and myself. I have thoroughly analyzed and tested the suspected vulnerability, and can see that it is not. I believe the issue that caused the server to go down was simply the security scanning tool overwhelming the server with to many requests, and the server not being configured properly for rather meager memory limitations. --- lib/WeBWorK/CourseEnvironment.pm | 1 + lib/WeBWorK/Utils/Routes.pm | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/WeBWorK/CourseEnvironment.pm b/lib/WeBWorK/CourseEnvironment.pm index a10ed8b6f6..f7ac2e9e0a 100644 --- a/lib/WeBWorK/CourseEnvironment.pm +++ b/lib/WeBWorK/CourseEnvironment.pm @@ -96,6 +96,7 @@ sub new { $seedVars->{pg_dir} //= $WeBWorK::SeedCE{pg_dir} // $ENV{PG_ROOT}; $seedVars->{courseName} ||= '___'; # prevents extraneous error messages + $seedVars->{courseName} =~ s/'.*$//; # The following line is a work around for a bug that occurs on some systems. See # https://rt.cpan.org/Public/Bug/Display.html?id=77916 and diff --git a/lib/WeBWorK/Utils/Routes.pm b/lib/WeBWorK/Utils/Routes.pm index 57cf125a9c..4cef1ae0b4 100644 --- a/lib/WeBWorK/Utils/Routes.pm +++ b/lib/WeBWorK/Utils/Routes.pm @@ -296,7 +296,7 @@ my %routeParameters = ( logout options instructor_tools problem_list) ], module => 'ProblemSets', - path => '/#courseID' + path => { '/#courseID' => [ courseID => qr/[\w-]*/ ] } }, logout => { @@ -429,7 +429,7 @@ my %routeParameters = ( instructor_problem_grader => { title => x('Manual Grader'), module => 'Instructor::ProblemGrader', - path => '/grader/#setID/#problemID' + path => '/grader/#setID/' }, instructor_add_users => { title => x('Add Users'), @@ -471,7 +471,7 @@ my %routeParameters = ( instructor_problem_editor_withset_withproblem => { title => '[_3]', module => 'Instructor::PGProblemEditor', - path => '/#problemID' + path => '/' }, instructor_scoring => { title => x('Scoring Tools'), @@ -503,7 +503,7 @@ my %routeParameters = ( instructor_problem_statistics => { title => '[_3]', module => 'Instructor::Stats', - path => '/#problemID' + path => '/' }, instructor_user_statistics => { title => '[_1]', @@ -570,7 +570,7 @@ my %routeParameters = ( title => '[_3]', children => [qw(show_me_another)], module => 'Problem', - path => '/#problemID', + path => '/', unrestricted => 1 }, show_me_another => { @@ -617,15 +617,22 @@ sub setup_content_generator_routes_recursive { my $action = $routeParameters{$child}{action} // 'go'; if ($routeParameters{$child}{children}) { - my $child_route = $route->under($routeParameters{$child}{path}, [ problemID => qr/\d+/ ])->name($child); + my $child_route = $route->under( + ref($routeParameters{$child}{path}) eq 'HASH' + ? %{ $routeParameters{$child}{path} } + : $routeParameters{$child}{path})->name($child); $child_route->any($routeParameters{$child}{methods} // (), '/')->to("$routeParameters{$child}{module}#$action") ->name($child); for (@{ $routeParameters{$child}{children} }) { setup_content_generator_routes_recursive($child_route, $_); } } else { - $route->any($routeParameters{$child}{methods} // (), $routeParameters{$child}{path}, [ problemID => qr/\d+/ ]) - ->to("$routeParameters{$child}{module}#$action")->name($child); + $route->any( + $routeParameters{$child}{methods} // (), + ref($routeParameters{$child}{path}) eq 'HASH' + ? %{ $routeParameters{$child}{path} } + : $routeParameters{$child}{path} + )->to("$routeParameters{$child}{module}#$action")->name($child); } return;