diff --git a/classes/File.php b/classes/File.php index 6ba80eb5f6..2115313a81 100644 --- a/classes/File.php +++ b/classes/File.php @@ -237,7 +237,7 @@ class File extends Managed_DataObject static function filename(Profile $profile, $origname, $mimetype) { - $ext = self::guessMimeExtension($mimetype); + $ext = self::guessMimeExtension($mimetype, $origname); // Normalize and make the original filename more URL friendly. $origname = basename($origname, ".$ext"); @@ -258,19 +258,54 @@ class File extends Managed_DataObject return $filename; } - static function guessMimeExtension($mimetype) + /** + * @param $mimetype The mimetype we've discovered for this file. + * @param $filename An optional filename which we can use on failure. + */ + static function guessMimeExtension($mimetype, $filename=null) { + $ext = null; try { + // first see if we know the extension for our mimetype $ext = common_supported_mime_to_ext($mimetype); - } catch (Exception $e) { - // We don't support this mimetype, but let's guess the extension - $matches = array(); - if (!preg_match('/\/([a-z0-9]+)/', mb_strtolower($mimetype), $matches)) { - throw new Exception('Malformed mimetype: '.$mimetype); + // we do, so use it! + return $ext; + } catch (Exception $e) { // FIXME: Make this exception more specific to "unknown mime=>ext relation" + // We don't know the extension for this mimetype, but let's guess. + + // If we are very liberal with uploads ($config['attachments']['supported'] === true) + // then we try to do some guessing based on the filename, if it was supplied. + if (!is_null($filename) && common_config('attachments', 'supported')===true + && preg_match('/^.+\.([A-Za-z0-9]+)$/', $filename, $matches)) { + // we matched on a file extension, so let's see if it means something. + $ext = mb_strtolower($matches[1]); + + $blacklist = common_config('attachments', 'extblacklist'); + // If we got an extension from $filename we want to check if it's in a blacklist + // so we avoid people uploading .php files etc. + if (array_key_exists($ext, $blacklist)) { + if (!is_string($blacklist[$ext])) { + // we don't have a safe replacement extension + throw ClientException(_('Blacklisted file extension.')); + } + common_debug('Found replaced extension for filename '._ve($filename).': '._ve($ext)); + + // return a safe replacement extension ('php' => 'phps' for example) + return $blacklist[$ext]; + } + // the attachment extension based on its filename was not blacklisted so it's ok to use it + return $ext; } - $ext = $matches[1]; } - return mb_strtolower($ext); + + // If nothing else has given us a result, try to extract it from + // the mimetype value (this turns .jpg to .jpeg for example...) + $matches = array(); + if (!preg_match('/\/([a-z0-9]+)/', mb_strtolower($mimetype), $matches)) { + throw new Exception('Malformed mimetype: '.$mimetype); + } + $ext = mb_strtolower($matches[1]); + return $ext; } /** diff --git a/lib/default.php b/lib/default.php index f8ce3bd4fe..b945c24e20 100644 --- a/lib/default.php +++ b/lib/default.php @@ -266,6 +266,10 @@ $default = 'show_html' => false, // show (filtered) text/html attachments (and oEmbed HTML etc.). Doesn't affect AJAX calls. 'show_thumbs' => true, // show thumbnails in notice lists for uploaded images, and photos and videos linked remotely that provide oEmbed info 'process_links' => true, // check linked resources for embeddable photos and videos; this will hit referenced external web sites when processing new messages. + 'extblacklist' => [ + 'php' => 'phps', + 'exe' => false, // this would deny any uploads to keep the "exe" file extension + ], ), 'thumbnail' => array('crop' => false, // overridden to true if thumb height === null diff --git a/lib/mediafile.php b/lib/mediafile.php index 9fe5432ad5..1e0fb39769 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -253,15 +253,15 @@ class MediaFile File::respectsQuota($scoped, $_FILES[$param]['size']); $mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']); + $basename = basename($_FILES[$param]['name']); switch (common_config('attachments', 'filename_base')) { case 'upload': - $basename = basename($_FILES[$param]['name']); $filename = File::filename($scoped, $basename, $mimetype); break; case 'hash': default: - $filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype); + $filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype, $basename); } $filepath = File::path($filename);