Security problem

Security problem

by Gustav W Delius -
Number of replies: 18

Our students are quite clever. Luckily they are also very honest and so they came to me with the following observation: they can download any uploaded file if they can guess its name. The page file.php will serve the document to them irrespective of whether the document has actually be made visible by the teacher.

This observation is potentially very valuable to our students which is why I am so impressed that they came to tell me about it. Many teachers have the habit of giving the documents containing the assigned problems a name containing "problems" in it. Usually they then name the corresponding document with the solutions similarly, just replacing "problems" by solutions. The teachers usually upload the solutions already long time before the assignment closes but don't make it visible. Unknown to the teachers these solutions are freely available to all the students who can guess this naming scheme.

Does anyone have an idea how to plug this security loophole?

Average of ratings: -
In reply to Gustav W Delius

Re: Security problem

by Martin Dougiamas -
Picture of Core developers Picture of Documentation writers Picture of Moodle HQ Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers
I can't really see a way around this if the files are to be uploaded ... there are many cases when course files must be accessible to students in that course even if they are not directly linked as an uploaded resources (eg files linked within another file).  I suppose you could tell your teachers to use random names like 2answers-74757458989.html

The best way is just not to upload them as files ... instead, use the internal formats (plain text or HTML resources). These are stored in the database and all the visible/non-visible security applies to them.
In reply to Gustav W Delius

Re: Security problem

by Bernard Boucher -

Hi Gustav,


here is a small hack of moodle/file.php that restrict to students direct access to an uploaded file if it is not included in a resource in the current course.

First try, only for testing and discussion.

Bye,

Bernard

//bb
if (!isteacher($course->id) and !isadmin() ) {
    if (!$resources = get_records_sql("SELECT *
        FROM {$CFG->prefix}resource
        WHERE course= $courseid
        AND reference
        LIKE '%$filename%'")) {
            notice("<center>You cannot access this item directly until it appears as a ressource     in  the course</center> ");
    }
}



In reply to Bernard Boucher

Re: Security problem

by Ronald Vyhmeister -
Wouldn't it just be easier to say if ((isteacher($course->id) or isadmin()) { ... do normal } else notice ("....."); Ron
In reply to Bernard Boucher

Re: Security problem

by Martin Dougiamas -
Picture of Core developers Picture of Documentation writers Picture of Moodle HQ Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers
Note that any files that the resource links to (like images, or other HTML files) will be blocked using this method...
In reply to Bernard Boucher

Re: Security problem

by Gustav W Delius -

Hi Bernard,

thank you very much for suggesting a solution. At first I thought your simple suggestion solved all the problems but unfortunately there are some difficulties:

  • The resource module is not the only one that should be allowed to access files in the file area legitimitely.
  • I would like hidden resources to be unavailable to students. There is unfortunately no entry in the resource table that allows one to find out whether the resource is hidden or not.
  • As Martin already mentioned, some files are referenced inside a resource through hyperlinks and they too should be available.

Nevertheless your reply gave me the feeling that it should be possible to do something to protect the file area from unauthorized access. So here is my suggestion:

  • introduce a new table that contains all uploaded files together with a flag stating whether they are public or not.
  • When a file is uploaded through the filemanager it is entered into the table and set to private.
  • The file manager should allow the teacher to toggle whether a file or a directory is private or public.
  • When a module instance is created that uses the file, the file is set to public by that module.
  • When a module instance is hidden then its files are set to private and when a module instance is made visible again its files are set back to public.
  • file.php should always check whether a file is public before serving it to a student.

It would of course be even better if one could use the operating system's permissions system to store the nature of the file rather than introducing an extra table. But I am not sure whether that can be done given that Unix and Windows handle permissions so differently.

Unfortunately the suggestion above is not as nicely localised as Bernard's suggestion. Rather it requires small changes in the code all over the place. This makes its implementation a bit difficult except for one of the official Moodle programmers. Perhaps this security feature should be sponsored through moodle.com. I wonder how much that would cost.

In reply to Gustav W Delius

Alternative solution to security problem

by Gustav W Delius -

There is actually an alternative to the solution I described earlier. The security problem was that any student can open a file in moodle's data directory by calling file.php with the correct URL. The solution would be to get rid of file.php and to do what file.php does currently instead by calling a function. That solves the security problem because student's won't be able to call this function.

There is still the problem that file.php is also used for links inside resources. Links can't be replaced by function calls so we can't get rid of file.php alltogether. So I am proposing to restrict file.php to have access only to a public subfolder of the file area or to files containing the string "public" in their name. All files that are referenced inside other resources should be in this subfolder or named accordingly.

In order to keep backwards compatibility I would introduce a configuration variable $CFG->securefilearea. If it is set to true then file.php is restricted as described above, otherwise file.php continues to have access to the entire file area.

This solution should be quite simple to implement. Can anyone see any problems with it?

In reply to Gustav W Delius

Re: Alternative solution to security problem

by Martin Dougiamas -
Picture of Core developers Picture of Documentation writers Picture of Moodle HQ Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers
A special subfolder is a good idea, for backward compatibility it would probably be slightly better to do it the other away around and have a folder for private files, perhaps called "private" ... that way everybody can migrate naturally to it without needing changes to things like assignment uploads, forum attachments etc
In reply to Martin Dougiamas

Re: Alternative solution to security problem

by Gustav W Delius -

You have a point there. However I need to fix the security problem on an existing site and I don't want to have to ask all lecturers to move around their files. In fact ideally I don't even want to tell them anything about this at all because it would be a bit embarassing for me, given that I had originally told them that their file area was private. blush.gif

So this afternoon I will fix all the modules that create links to file.php so that instead they produce links to their own version of file.php which is called with information about the module instance rather than with the URL of the file. When that all works I will restrict the old file.php to check on $CFG->securefilearea to decide whether to deny access to a private folder (as you suggest) or to deny access to everything except the public folder (as I will have to do on my site where teachers have already uploaded sensitive material).

In reply to Martin Dougiamas

Re: Alternative solution to security problem

by Henrik Kaipe -
As some of you know, there are also other drawbacks for file.php. One of them is that it stops quiz images on questions that are published on one course to be viewed on another course.
The attached file quizfile.php is supposed to solve this problem by being used instead of file.php on quizes. (quizfile.php has not yet been committed or tested)

In the source you can see that quizfile.php first checks that the user, if student, is actually taking or reviewing a specified quiz as well as that a specified question is on that quiz and also is supposed to show the specified file.

A similar approach could be used for resources by creating resourcefile.php etc. In the end we could ban students from seeing course material using file.php.

---
Another thing - In the end of quizfile.php there is a part where the actual file is returned with the proper set of headers, mimesettings etc. This part is identical with the equivalent part in file.php. I think it should be separated out into a new function in mimetypes.php - something like
function print_file_with_headers($filepath, $lifetime=86400)
Therewith, file.php and quizfile.php (and perhaps resourcefile.php) would only concentrate on security matters and the identification of the file while the new function takes care of header settings.
Does anyone with the proper write privileges want to do that?
In reply to Henrik Kaipe

Re: Alternative solution to security problem

by Gustav W Delius -

I have actually already started on doing just what you are suggesting. It is good to see that Moodler's minds appear to be in synchrony smiley.gif.

My function was actually called moodle_show_file but I will change it to print_file_with_headers because that is more in line with moodle conventions. Do you have a particular reason to be able to pass the lifetime as an argument?

I don't actually have write privileges to CVS, I was planning to just post what I have done here and hope that someone else will write it to CVS.

In reply to Gustav W Delius

Re: Alternative solution to security problem

by Henrik Kaipe -
"Do you have a particular reason to be able to pass the lifetime as an argument?"

Not really, I have only seen that it was defined in the beginning of file.php and figured that it was put there in order to make it easy to change for local installations.
By defining it with a default value in the signature of print_file_with_headers I thought I would accomplish the same level of flexibility.
In reply to Henrik Kaipe

Re: Alternative solution to security problem

by Martin Dougiamas -
Picture of Core developers Picture of Documentation writers Picture of Moodle HQ Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers
The existing function style_sheet_setup() in lib/weblib.php does this too.  smiley.gif
In reply to Martin Dougiamas

Re: Alternative solution to security problem

by Gustav W Delius -

All I know about coding in PHP I have learned by studying your Moodle code. Clearly there is still a lot more I can learn by studying it further. All my Sunday afternoons are thus booked for the forseeable future. cool.gif

In reply to Henrik Kaipe

setting defaults through optional arguments

by Gustav W Delius -

Hi Henrik,

you notice that I am not a coder in real life blush.gif. Setting the default through an optional argument to the function rather than assigning it inside the function definition is of course a neat idea smiley.gif.

In reply to Gustav W Delius

Re: Security problem

by Martin Dougiamas -
Picture of Core developers Picture of Documentation writers Picture of Moodle HQ Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers
Your description of adding metadata to files is on the right track, and approaching the complete content management system which is in my top 5 list of big plumbing jobs that need doing.  This should be designed to work equally well on databases or filesystems (to fix the safe-mode problems), so I think the current file structure will be changing, too.  Other problems to be solved at the same time include cross-course sharing of resources, student file management, group file management, versioning of files, collaborative editing, WebDAV access etc.

If possible, I'd prefer to find an existing open source solution and bolt it on, but the several I've looked at closely so far were quite cumbersome.
In reply to Gustav W Delius

Re: Security problem

by Bernard Boucher -

Good work for a Sunday moodlers.

Here is a bigger hack modifying only file.php but correcting many ( it is not completed but it give a good idea on how to finish it that way ).

It use log to check from where came the student.

It also treat not visible resources.

Is there things that are not possible that way? ( please don't tell things that are not done wink it is not finished).

Have a good cogitation,

Bernard

p.s. I lost some indentation in the txt file angry.gif

//bb
    if (!isteacher($course->id) and !isadmin() ) {
        $logs = get_records("log", "userid", $USER->id);

     foreach ($logs as $log) {  // sure it exist a better way to get the last one
        }

     switch ($log->module) {
            case "course":
                $label = get_records_sql("SELECT *
                             FROM {$CFG->prefix}label
                             WHERE course= $courseid
              AND content
                             LIKE '%$filename%'"); 
    
    $section = get_records_sql("SELECT *
                                FROM {$CFG->prefix}course_sections
                                WHERE course= $courseid
                    AND summary
                                LIKE '%$filename%'"); 

    $resource = get_record_sql("SELECT *
                                    FROM {$CFG->prefix}resource
                                    WHERE course= $courseid
                        AND reference
                                    LIKE '%$filename%'");

    
    if ($resource )  {
                   $module = get_record("course_modules", "course", $courseid,"module", 6,"instance",                       $resource->id);
                }

    
    if (!$section and !$label and (!$resource or !$module->visible)) {

                    notice("<center>You cannot access this item directly until it appears in the course</center> ");
                }
   break;

   case "resource":
                if (!$resource = get_records_sql("SELECT *
                                         FROM {$CFG->prefix}resource
                                         WHERE course= $courseid
                 AND reference
                                         LIKE '%$filename%'"))  {
                    notice("<center>You cannot access this item directly until it appears in the course</center> ");
                }
   break;
  
            case "quiz":
                if (!$quiz = get_records_sql("SELECT *
                                 FROM {$CFG->prefix}quiz_questions
                                 WHERE 1
         AND  image LIKE '%$filename%'
                 OR
              questiontext  LIKE '%$filename%'") )  {
                        notice("<center>You cannot access this item directly until it appears in the course</center> ");
                }
   break;
     default:
    $resource = get_record_sql("SELECT *
                                    FROM {$CFG->prefix}resource
                                    WHERE course= $courseid
                        AND reference
                                    LIKE '%$filename%'");

    
    if ($resource )  {
                   $module = get_record("course_modules", "course", $courseid,"module", 6,"instance",                       $resource->id);
                }

    if (!$resource or !$module->visible) {
     notice("<center>You cannot access this item directly until it appears in the course</center> ");
                }
            break;
        }
    }

In reply to Gustav W Delius

Re: Security problem

by Paul Mazza -
I know that this thread is over a year old, but...

I, too, am wrestling with this security issue. Here is an idea that may be easily workable.

I propose that a new table be created with the following schema...

CREATE TABLE filelinks (
file_id CHAR(30) PRIMARY KEY,
file_name CHAR(256),
);

Then, any time a new "Uploaded File" resource is added, a row would be inserted into this table as follows...

A random string of characters would be generated as the "file_id"
The desired file name would, of course, be assigned to "file_name"
An attempt to insert the row would be made
If the attempt succeeds, the process would be complete
If the attempt does not succeed, due to a duplicate primary, a new random string
would be generated and a new attempt would be made

The actual resource row would include the "file_id" value in its "name" column, instead of the actual file name.

Then, the file.php script would be modified slightly so as not to expect a filename path, but instead to expect a "file_id". When invoked, it would do a simple, quick retrieval of the row in the "filelinks" table where the file_id matches.

If now row is found, the script would report a friendly message like "You are attempting to hack the CMS. Please see your instructor for proper disciplinary action."

If a row is found, the script would perform its normal processing of the file whose name is retrieved.

The only other mods that would need to be made would be for when a resource is edited or deleted.

This solution would obscure the filenames completely from the student and, thus, prohibit them from guessing the name of non-assigned resources.

Any thoughts?