SimpleMachines /
SMF2.1
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | |||
| 3 | /** |
||
| 4 | * This file handles avatar and attachment requests. The whole point of this file is to reduce the loaded stuff to show an image. |
||
| 5 | * |
||
| 6 | * Simple Machines Forum (SMF) |
||
| 7 | * |
||
| 8 | * @package SMF |
||
| 9 | * @author Simple Machines http://www.simplemachines.org |
||
| 10 | * @copyright 2018 Simple Machines and individual contributors |
||
| 11 | * @license http://www.simplemachines.org/about/smf/license.php BSD |
||
| 12 | * |
||
| 13 | * @version 2.1 Beta 4 |
||
| 14 | */ |
||
| 15 | |||
| 16 | if (!defined('SMF')) |
||
| 17 | die('No direct access...'); |
||
| 18 | |||
| 19 | /** |
||
| 20 | * Downloads an avatar or attachment based on $_GET['attach'], and increments the download count. |
||
| 21 | * It requires the view_attachments permission. |
||
| 22 | * It disables the session parser, and clears any previous output. |
||
| 23 | * It depends on the attachmentUploadDir setting being correct. |
||
| 24 | * It is accessed via the query string ?action=dlattach. |
||
| 25 | * Views to attachments do not increase hits and are not logged in the "Who's Online" log. |
||
| 26 | */ |
||
| 27 | function showAttachment() |
||
| 28 | { |
||
| 29 | global $smcFunc, $modSettings, $maintenance, $context; |
||
| 30 | |||
| 31 | // Some defaults that we need. |
||
| 32 | $context['character_set'] = empty($modSettings['global_character_set']) ? (empty($txt['lang_character_set']) ? 'ISO-8859-1' : $txt['lang_character_set']) : $modSettings['global_character_set']; |
||
| 33 | $context['utf8'] = $context['character_set'] === 'UTF-8'; |
||
| 34 | |||
| 35 | // An early hook to set up global vars, clean cache and other early process. |
||
| 36 | call_integration_hook('integrate_pre_download_request'); |
||
| 37 | |||
| 38 | // This is done to clear any output that was made before now. |
||
| 39 | ob_end_clean(); |
||
| 40 | |||
| 41 | if (!empty($modSettings['enableCompressedOutput']) && !headers_sent() && ob_get_length() == 0) |
||
| 42 | { |
||
| 43 | View Code Duplication | if (@ini_get('zlib.output_compression') == '1' || @ini_get('output_handler') == 'ob_gzhandler') |
|
| 44 | $modSettings['enableCompressedOutput'] = 0; |
||
| 45 | |||
| 46 | else |
||
| 47 | ob_start('ob_gzhandler'); |
||
| 48 | } |
||
| 49 | |||
| 50 | if (empty($modSettings['enableCompressedOutput'])) |
||
| 51 | { |
||
| 52 | ob_start(); |
||
| 53 | header('content-encoding: none'); |
||
| 54 | } |
||
| 55 | |||
| 56 | // Better handling. |
||
| 57 | $attachId = isset($_REQUEST['attach']) ? (int) $_REQUEST['attach'] : (int) (isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0); |
||
| 58 | |||
| 59 | // We need a valid ID. |
||
| 60 | if (empty($attachId)) |
||
| 61 | { |
||
| 62 | header('HTTP/1.0 404 File Not Found'); |
||
| 63 | die('404 File Not Found'); |
||
| 64 | } |
||
| 65 | |||
| 66 | // A thumbnail has been requested? madness! madness I say! |
||
| 67 | $preview = isset($_REQUEST['preview']) ? $_REQUEST['preview'] : (isset($_REQUEST['type']) && $_REQUEST['type'] == 'preview' ? $_REQUEST['type'] : 0); |
||
| 68 | $showThumb = isset($_REQUEST['thumb']) || !empty($preview); |
||
| 69 | $attachTopic = isset($_REQUEST['topic']) ? (int) $_REQUEST['topic'] : 0; |
||
| 70 | |||
| 71 | // No access in strict maintenance mode or you don't have permission to see attachments. |
||
| 72 | if ((!empty($maintenance) && $maintenance == 2) || !allowedTo('view_attachments')) |
||
| 73 | { |
||
| 74 | header('HTTP/1.0 404 File Not Found'); |
||
| 75 | die('404 File Not Found'); |
||
| 76 | } |
||
| 77 | |||
| 78 | // Use cache when possible. |
||
| 79 | if (($cache = cache_get_data('attachment_lookup_id-' . $attachId)) != null) |
||
| 80 | list($file, $thumbFile) = $cache; |
||
| 81 | |||
| 82 | // Get the info from the DB. |
||
| 83 | if (empty($file) || empty($thumbFile) && !empty($file['id_thumb'])) |
||
| 84 | { |
||
| 85 | // Do we have a hook wanting to use our attachment system? We use $attachRequest to prevent accidental usage of $request. |
||
| 86 | $attachRequest = null; |
||
| 87 | call_integration_hook('integrate_download_request', array(&$attachRequest)); |
||
| 88 | if (!is_null($attachRequest) && $smcFunc['db_is_resource']($attachRequest)) |
||
| 89 | $request = $attachRequest; |
||
| 90 | |||
| 91 | else |
||
| 92 | { |
||
| 93 | // Make sure this attachment is on this board and load its info while we are at it. |
||
| 94 | $request = $smcFunc['db_query']('', ' |
||
| 95 | SELECT id_folder, filename, file_hash, fileext, id_attach, id_thumb, attachment_type, mime_type, approved, id_msg |
||
| 96 | FROM {db_prefix}attachments |
||
| 97 | WHERE id_attach = {int:attach} |
||
| 98 | LIMIT 1', |
||
| 99 | array( |
||
| 100 | 'attach' => $attachId, |
||
| 101 | ) |
||
| 102 | ); |
||
| 103 | } |
||
| 104 | |||
| 105 | // No attachment has been found. |
||
| 106 | if ($smcFunc['db_num_rows']($request) == 0) |
||
| 107 | { |
||
| 108 | header('HTTP/1.0 404 File Not Found'); |
||
| 109 | die('404 File Not Found'); |
||
| 110 | } |
||
| 111 | |||
| 112 | $file = $smcFunc['db_fetch_assoc']($request); |
||
| 113 | $smcFunc['db_free_result']($request); |
||
| 114 | |||
| 115 | // If theres a message ID stored, we NEED a topic ID. |
||
| 116 | if (!empty($file['id_msg']) && empty($attachTopic) && empty($preview)) |
||
| 117 | { |
||
| 118 | header('HTTP/1.0 404 File Not Found'); |
||
| 119 | die('404 File Not Found'); |
||
| 120 | } |
||
| 121 | |||
| 122 | // Previews doesn't have this info. |
||
| 123 | if (empty($preview)) |
||
| 124 | { |
||
| 125 | $request2 = $smcFunc['db_query']('', ' |
||
| 126 | SELECT a.id_msg |
||
| 127 | FROM {db_prefix}attachments AS a |
||
| 128 | INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg AND m.id_topic = {int:current_topic}) |
||
| 129 | INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board}) |
||
| 130 | WHERE a.id_attach = {int:attach} |
||
| 131 | LIMIT 1', |
||
| 132 | array( |
||
| 133 | 'attach' => $attachId, |
||
| 134 | 'current_topic' => $attachTopic, |
||
| 135 | ) |
||
| 136 | ); |
||
| 137 | |||
| 138 | // The provided topic must match the one stored in the DB for this particular attachment, also. |
||
| 139 | if ($smcFunc['db_num_rows']($request2) == 0) |
||
| 140 | { |
||
| 141 | header('HTTP/1.0 404 File Not Found'); |
||
| 142 | die('404 File Not Found'); |
||
| 143 | } |
||
| 144 | |||
| 145 | $smcFunc['db_free_result']($request2); |
||
| 146 | } |
||
| 147 | |||
| 148 | // set filePath and ETag time |
||
| 149 | $file['filePath'] = getAttachmentFilename($file['filename'], $attachId, $file['id_folder'], false, $file['file_hash']); |
||
| 150 | // ensure variant attachment compatibility |
||
| 151 | $filePath = pathinfo($file['filePath']); |
||
| 152 | $file['filePath'] = !file_exists($file['filePath']) ? substr($file['filePath'], 0, -(strlen($filePath['extension']) + 1)) : $file['filePath']; |
||
| 153 | $file['etag'] = '"' . md5_file($file['filePath']) . '"'; |
||
| 154 | |||
| 155 | // now get the thumbfile! |
||
| 156 | $thumbFile = array(); |
||
| 157 | if (!empty($file['id_thumb'])) |
||
| 158 | { |
||
| 159 | $request = $smcFunc['db_query']('', ' |
||
| 160 | SELECT id_folder, filename, file_hash, fileext, id_attach, attachment_type, mime_type, approved, id_member |
||
| 161 | FROM {db_prefix}attachments |
||
| 162 | WHERE id_attach = {int:thumb_id} |
||
| 163 | LIMIT 1', |
||
| 164 | array( |
||
| 165 | 'thumb_id' => $file['id_thumb'], |
||
| 166 | ) |
||
| 167 | ); |
||
| 168 | |||
| 169 | $thumbFile = $smcFunc['db_fetch_assoc']($request); |
||
| 170 | $smcFunc['db_free_result']($request); |
||
| 171 | |||
| 172 | // Got something! replace the $file var with the thumbnail info. |
||
| 173 | View Code Duplication | if ($thumbFile) |
|
| 174 | { |
||
| 175 | $attachId = $thumbFile['id_attach']; |
||
| 176 | |||
| 177 | // set filePath and ETag time |
||
| 178 | $thumbFile['filePath'] = getAttachmentFilename($thumbFile['filename'], $attachId, $thumbFile['id_folder'], false, $thumbFile['file_hash']); |
||
| 179 | $thumbFile['etag'] = '"' . md5_file($thumbFile['filePath']) . '"'; |
||
| 180 | } |
||
| 181 | } |
||
| 182 | |||
| 183 | // Cache it. |
||
| 184 | if (!empty($file) || !empty($thumbFile)) |
||
| 185 | cache_put_data('attachment_lookup_id-' . $file['id_attach'], array($file, $thumbFile), mt_rand(850, 900)); |
||
| 186 | } |
||
| 187 | |||
| 188 | // Replace the normal file with its thumbnail if it has one! |
||
| 189 | if (!empty($showThumb) && !empty($thumbFile)) |
||
| 190 | $file = $thumbFile; |
||
| 191 | |||
| 192 | // No point in a nicer message, because this is supposed to be an attachment anyway... |
||
| 193 | if (!file_exists($file['filePath'])) |
||
| 194 | { |
||
| 195 | header((preg_match('~HTTP/1\.[01]~i', $_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0') . ' 404 Not Found'); |
||
| 196 | header('content-type: text/plain; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set'])); |
||
| 197 | |||
| 198 | // We need to die like this *before* we send any anti-caching headers as below. |
||
| 199 | die('File not found.'); |
||
| 200 | } |
||
| 201 | |||
| 202 | // If it hasn't been modified since the last time this attachment was retrieved, there's no need to display it again. |
||
| 203 | if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) |
||
| 204 | { |
||
| 205 | list($modified_since) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE']); |
||
| 206 | if (strtotime($modified_since) >= filemtime($file['filePath'])) |
||
| 207 | { |
||
| 208 | ob_end_clean(); |
||
| 209 | |||
| 210 | // Answer the question - no, it hasn't been modified ;). |
||
| 211 | header('HTTP/1.1 304 Not Modified'); |
||
| 212 | exit; |
||
| 213 | } |
||
| 214 | } |
||
| 215 | |||
| 216 | // Check whether the ETag was sent back, and cache based on that... |
||
| 217 | $eTag = '"' . substr($_REQUEST['attach'] . $file['filePath'] . filemtime($file['filePath']), 0, 64) . '"'; |
||
| 218 | if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false) |
||
| 219 | { |
||
| 220 | ob_end_clean(); |
||
| 221 | |||
| 222 | header('HTTP/1.1 304 Not Modified'); |
||
| 223 | exit; |
||
| 224 | } |
||
| 225 | |||
| 226 | // If this is a partial download, we need to determine what data range to send |
||
| 227 | $range = 0; |
||
| 228 | $size = filesize($file['filePath']); |
||
| 229 | if (isset($_SERVER['HTTP_RANGE'])) |
||
| 230 | { |
||
| 231 | list($a, $range) = explode("=", $_SERVER['HTTP_RANGE'], 2); |
||
| 232 | list($range) = explode(",", $range, 2); |
||
| 233 | list($range, $range_end) = explode("-", $range); |
||
| 234 | $range = intval($range); |
||
| 235 | $range_end = !$range_end ? $size - 1 : intval($range_end); |
||
| 236 | $new_length = $range_end - $range + 1; |
||
| 237 | } |
||
| 238 | |||
| 239 | // Update the download counter (unless it's a thumbnail or resuming an incomplete download). |
||
| 240 | if ($file['attachment_type'] != 3 && empty($showThumb) && $range === 0) |
||
| 241 | $smcFunc['db_query']('', ' |
||
| 242 | UPDATE {db_prefix}attachments |
||
| 243 | SET downloads = downloads + 1 |
||
| 244 | WHERE id_attach = {int:id_attach}', |
||
| 245 | array( |
||
| 246 | 'id_attach' => $attachId, |
||
| 247 | ) |
||
| 248 | ); |
||
| 249 | |||
| 250 | // Send the attachment headers. |
||
| 251 | header('pragma: '); |
||
| 252 | |||
| 253 | if (!isBrowser('gecko')) |
||
| 254 | header('content-transfer-encoding: binary'); |
||
| 255 | |||
| 256 | header('expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT'); |
||
| 257 | header('last-modified: ' . gmdate('D, d M Y H:i:s', filemtime($file['filePath'])) . ' GMT'); |
||
| 258 | header('accept-ranges: bytes'); |
||
| 259 | header('connection: close'); |
||
| 260 | header('etag: ' . $eTag); |
||
|
0 ignored issues
–
show
|
|||
| 261 | |||
| 262 | // Make sure the mime type warrants an inline display. |
||
| 263 | if (isset($_REQUEST['image']) && !empty($file['mime_type']) && strpos($file['mime_type'], 'image/') !== 0) |
||
| 264 | unset($_REQUEST['image']); |
||
| 265 | |||
| 266 | // Does this have a mime type? |
||
| 267 | elseif (!empty($file['mime_type']) && (isset($_REQUEST['image']) || !in_array($file['fileext'], array('jpg', 'gif', 'jpeg', 'x-ms-bmp', 'png', 'psd', 'tiff', 'iff')))) |
||
| 268 | header('content-type: ' . strtr($file['mime_type'], array('image/bmp' => 'image/x-ms-bmp'))); |
||
| 269 | |||
| 270 | else |
||
| 271 | { |
||
| 272 | header('content-type: ' . (isBrowser('ie') || isBrowser('opera') ? 'application/octetstream' : 'application/octet-stream')); |
||
| 273 | if (isset($_REQUEST['image'])) |
||
| 274 | unset($_REQUEST['image']); |
||
| 275 | } |
||
| 276 | |||
| 277 | // Convert the file to UTF-8, cuz most browsers dig that. |
||
| 278 | $utf8name = !$context['utf8'] && function_exists('iconv') ? iconv($context['character_set'], 'UTF-8', $file['filename']) : (!$context['utf8'] && function_exists('mb_convert_encoding') ? mb_convert_encoding($file['filename'], 'UTF-8', $context['character_set']) : $file['filename']); |
||
| 279 | $disposition = !isset($_REQUEST['image']) ? 'attachment' : 'inline'; |
||
| 280 | |||
| 281 | // Different browsers like different standards... |
||
| 282 | if (isBrowser('firefox')) |
||
| 283 | header('content-disposition: ' . $disposition . '; filename*=UTF-8\'\'' . rawurlencode(preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $utf8name))); |
||
| 284 | |||
| 285 | elseif (isBrowser('opera')) |
||
| 286 | header('content-disposition: ' . $disposition . '; filename="' . preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $utf8name) . '"'); |
||
| 287 | |||
| 288 | elseif (isBrowser('ie')) |
||
| 289 | header('content-disposition: ' . $disposition . '; filename="' . urlencode(preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $utf8name)) . '"'); |
||
| 290 | |||
| 291 | else |
||
| 292 | header('content-disposition: ' . $disposition . '; filename="' . $utf8name . '"'); |
||
| 293 | |||
| 294 | // If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE. |
||
| 295 | if (!isset($_REQUEST['image']) && in_array($file['fileext'], array('gif', 'jpg', 'bmp', 'png', 'jpeg', 'tiff'))) |
||
| 296 | header('cache-control: no-cache'); |
||
| 297 | |||
| 298 | else |
||
| 299 | header('cache-control: max-age=' . (525600 * 60) . ', private'); |
||
| 300 | |||
| 301 | // Multipart and resuming support |
||
| 302 | if (isset($_SERVER['HTTP_RANGE'])) |
||
| 303 | { |
||
| 304 | header("HTTP/1.1 206 Partial Content"); |
||
| 305 | header("content-length: $new_length"); |
||
| 306 | header("content-range: bytes $range-$range_end/$size"); |
||
| 307 | } |
||
| 308 | else |
||
| 309 | header("content-length: " . $size); |
||
|
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
The string literal
content-length: does not require double quotes, as per coding-style, please use single quotes.
PHP provides two ways to mark string literals. Either with single quotes String literals in single quotes on the other hand are evaluated very literally and the only two
characters that needs escaping in the literal are the single quote itself ( Double quoted string literals may contain other variables or more complex escape sequences. <?php
$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";
print $doubleQuoted;
will print an indented: If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear. For more information on PHP string literals and available escape sequences see the PHP core documentation. Loading history...
|
|||
| 310 | |||
| 311 | |||
| 312 | // Try to buy some time... |
||
| 313 | @set_time_limit(600); |
||
| 314 | |||
| 315 | // For multipart/resumable downloads, send the requested chunk(s) of the file |
||
| 316 | if (isset($_SERVER['HTTP_RANGE'])) |
||
| 317 | { |
||
| 318 | while (@ob_get_level() > 0) |
||
| 319 | @ob_end_clean(); |
||
| 320 | |||
| 321 | // 40 kilobytes is a good-ish amount |
||
| 322 | $chunksize = 40 * 1024; |
||
| 323 | $bytes_sent = 0; |
||
| 324 | |||
| 325 | $fp = fopen($file['filePath'], 'rb'); |
||
| 326 | |||
| 327 | fseek($fp, $range); |
||
| 328 | |||
| 329 | while (!feof($fp) && (!connection_aborted()) && ($bytes_sent < $new_length)) |
||
| 330 | { |
||
| 331 | $buffer = fread($fp, $chunksize); |
||
| 332 | echo($buffer); |
||
| 333 | flush(); |
||
| 334 | $bytes_sent += strlen($buffer); |
||
| 335 | } |
||
| 336 | fclose($fp); |
||
| 337 | } |
||
| 338 | |||
| 339 | // Since we don't do output compression for files this large... |
||
| 340 | elseif ($size > 4194304) |
||
| 341 | { |
||
| 342 | // Forcibly end any output buffering going on. |
||
| 343 | while (@ob_get_level() > 0) |
||
| 344 | @ob_end_clean(); |
||
| 345 | |||
| 346 | $fp = fopen($file['filePath'], 'rb'); |
||
| 347 | while (!feof($fp)) |
||
| 348 | { |
||
| 349 | echo fread($fp, 8192); |
||
| 350 | flush(); |
||
| 351 | } |
||
| 352 | fclose($fp); |
||
| 353 | } |
||
| 354 | |||
| 355 | // On some of the less-bright hosts, readfile() is disabled. It's just a faster, more byte safe, version of what's in the if. |
||
| 356 | elseif (@readfile($file['filePath']) === null) |
||
| 357 | echo file_get_contents($file['filePath']); |
||
| 358 | |||
| 359 | die(); |
||
| 360 | } |
||
| 361 | |||
| 362 | ?> |
'etag: ' . $eTagcan contain request data and is used in response header context(s) leading to a potential security vulnerability.1 path for user data to reach this point
$_REQUEST,and$_REQUEST['attach'] . $file['filePath'] . filemtime($file['filePath'])is passed through substr(), and$eTagis assignedin Sources/ShowAttachments.php on line 217
Response Splitting Attacks
Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.
General Strategies to prevent injection
In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:
if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) { throw new \InvalidArgumentException('This input is not allowed.'); }For numeric data, we recommend to explicitly cast the data: