View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0003540 | mantisbt | security | public | 2004-02-07 13:53 | 2006-10-09 11:54 |
Reporter | TomVogt | Assigned To | thraxisp | ||
Priority | normal | Severity | major | Reproducibility | always |
Status | closed | Resolution | fixed | ||
Fixed in Version | 0.19.0rc1 | ||||
Summary | 0003540: Arbitrary code execution through uploads | ||||
Description | If the upload path is in the webroot, .php files can be uploaded as attachments to bug reports and will be executed when accessed. | ||||
Tags | No tags attached. | ||||
Attached Files | filename.diff (3,596 bytes)
Index: core/file_api.php =================================================================== RCS file: /cvsroot/mantisbt/mantisbt/core/file_api.php,v retrieving revision 1.52 diff -u -r1.52 file_api.php --- core/file_api.php 21 Aug 2004 00:13:52 -0000 1.52 +++ core/file_api.php 26 Aug 2004 00:53:59 -0000 @@ -423,6 +423,47 @@ } # -------------------- + # Generate a string to use as the identifier for the file + # It is not guaranteed to be unique and should be checked + # The string returned should be 32 characters in length + function file_generate_name( $p_seed ) { + $t_val = md5( $p_seed . time() ); + + return substr( $t_val, 0, 32 ); + } + + # -------------------- + # Generate a UNIQUE string to use as the identifier for the file + # The string returned should be 64 characters in length + function file_generate_unique_name( $p_seed , $p_filepath ) { + do { + $t_string = file_generate_name( $p_seed ); + } while ( !file_is_name_unique( $t_string , $p_filepath ) ); + + return $t_string; + } + + # -------------------- + # Return true if the file name identifier is unique, false otherwise + function file_is_name_unique( $p_name , $p_filepath ) { + $t_file_table = config_get( 'mantis_bug_file_table' ); + + $c_name = db_prepare_string( $p_filepath . $p_name ); + + $query = "SELECT COUNT(*) + FROM $t_file_table + WHERE diskfile='$c_name'"; + $result = db_query( $query ); + $t_count = db_result( $result ); + + if ( $t_count > 0 ) { + return false; + } else { + return true; + } + } + + # -------------------- function file_add( $p_bug_id, $p_tmp_file, $p_file_name, $p_file_type='' ) { $c_bug_id = db_prepare_int( $p_bug_id ); $c_file_type = db_prepare_string( $p_file_type ); @@ -440,9 +481,10 @@ # prepare variables for insertion $t_file_path = project_get_field( $t_project_id, 'file_path' ); $c_file_path = db_prepare_string( $t_file_path ); + $c_new_file_name = db_prepare_string( $p_file_name ); - $t_new_file_name = $t_bug_id . '-' . $p_file_name; - $c_new_file_name = db_prepare_string( $t_new_file_name ); + $t_disk_file_name = $t_file_path . file_generate_unique_name( $t_bug_id . '-' . $p_file_name, $t_file_path ); + $c_disk_file_name = db_prepare_string( $t_disk_file_name ); if ( is_readable ( $p_tmp_file ) ) { $t_file_size = filesize( $p_tmp_file ); @@ -462,15 +504,15 @@ case DISK: file_ensure_valid_upload_path( $t_file_path ); - if ( !file_exists( $t_file_path . $t_new_file_name ) ) { + if ( !file_exists( $t_disk_file_name ) ) { if ( FTP == $t_method ) { $conn_id = file_ftp_connect(); - file_ftp_put ( $conn_id, $t_new_file_name, $p_tmp_file ); + file_ftp_put ( $conn_id, $t_disk_file_name, $p_tmp_file ); file_ftp_disconnect ( $conn_id ); } umask( 0333 ); # make read only - move_uploaded_file( $p_tmp_file, $t_file_path . $t_new_file_name ); + move_uploaded_file( $p_tmp_file, $t_disk_file_name ); $c_content = ''; } else { @@ -487,7 +529,7 @@ $query = "INSERT INTO $t_bug_file_table (bug_id, title, description, diskfile, filename, folder, filesize, file_type, date_added, content) VALUES - ($c_bug_id, '', '', '$c_file_path$c_new_file_name', '$c_new_file_name', '$c_file_path', $c_file_size, '$c_file_type', " . db_now() .", '$c_content')"; + ($c_bug_id, '', '', '$c_disk_file_name', '$c_new_file_name', '$c_file_path', $c_file_size, '$c_file_type', " . db_now() .", '$c_content')"; db_query( $query ); # updated the last_updated date | ||||
related to | 0003349 | closed | thraxisp | File upload suggestion |
related to | 0001137 | closed | thraxisp | using per bug subdirectories for file uploading |
related to | 0004435 | closed | vboctor | Auto-preview of attached images is broken |
related to | 0009454 | new | upload a file with his name or a hash conversion ? | |
child of | 0003987 | closed | vboctor | Mantis 0.19.0 Release |
Lets put the best approach for fixing this. I will put some ideas and you are welcome to add others:
|
|
Reminder sent to jlatour Jeroen, what do you think about this one? |
|
You can make PHP a disallowed extension, but it will still allow people to read the files of any bug, or execute ASP/JSP/etc. code. So I think perhaps warn when it's in the document root with admin/check.php? We could add a .htaccess file, but we don't package an attachment directory, do we? |
|
At least in 0.17, putting the upload path outside the webroot doesn't work well, as stuff isn't shown anymore. But it would definitely be the best solution, with a short php script that just passes the content through. I am in contact with the guy who compromised my database using this problem, so if there are any more details required, I can probably ask him. |
|
Is there anything we can or are going to do about this, apart from perhaps a note in the documentation or a check in check.php? |
|
well, a link to download an attachment looks like : /file_download.php?file_id=320&type=bug So, neither the location or the name of the file are required to be known at any time. e.g. why not store an uploaded test.php file as '320' or even md5('test.php') as the filename ? As the files should only be grabbed via file_download.php (as this checks access), seems like a reasonable option imo |
|
Yes, I think that'll work, at least to prevent this arbitrary code execution problem. Good thinking! There still remains the problem of files being readable to anyone though. Do we still need to add a check for that? |
|
Possibly cover the 'default' case? And modify whatever the hash is so it takes an unknown value when hashing (e.g. rand(0,100000) + filename). Looking at the mantis_bug_file_table, which is currently bug_id, title,description,diskfile,filename,folder,filesize,file_type,date_added,content where diskfile/filename/folder/file_type are all 250 chars we could probably optimise this in the process (depending on whether there's a reason for all those fields) In the above scenario, diskfile would become 32 bytes (e.g. a md5 hash), folder would largely become pointless storing (as if the filenames were hashed, you wouldn't be able to download them directly (problem?), so the folder structure that we used to store files would become largely irrelevant to the user. Questions being: |
|
Yeah, I guess we could do that, but I still think we should add a check for this. Might just prevent a few more leaks. The file database probably has to be looked at. And if we're going to reduce the size of the disk filename field, we'll have to convert existing filenames. |
|
i've linked this to 'possible to include in next release (after 0.19' |
|
Actually, this sounds more like something for 0.19.0 to me. |
|
Proposed diff to file_api.php attached. |
|
so for this:
Would seem to be accurate of where we are. |
|
Fixed in CVS. The hashed filename is stored in the 'diskfile' column. |
|