Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/media/PNGMetadataExtractor.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * PNG frame counter and metadata extractor.
4
 *
5
 * Slightly derived from GIFMetadataExtractor.php
6
 * Deliberately not using MWExceptions to avoid external dependencies, encouraging
7
 * redistribution.
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 * @ingroup Media
26
 */
27
28
/**
29
 * PNG frame counter.
30
 *
31
 * @ingroup Media
32
 */
33
class PNGMetadataExtractor {
34
	/** @var string */
35
	private static $pngSig;
36
37
	/** @var int */
38
	private static $crcSize;
39
40
	/** @var array */
41
	private static $textChunks;
42
43
	const VERSION = 1;
44
	const MAX_CHUNK_SIZE = 3145728; // 3 megabytes
45
46
	static function getMetadata( $filename ) {
47
		self::$pngSig = pack( "C8", 137, 80, 78, 71, 13, 10, 26, 10 );
48
		self::$crcSize = 4;
49
		/* based on list at http://owl.phy.queensu.ca/~phil/exiftool/TagNames/PNG.html#TextualData
50
		 * and https://www.w3.org/TR/PNG/#11keywords
51
		 */
52
		self::$textChunks = [
53
			'xml:com.adobe.xmp' => 'xmp',
54
			# Artist is unofficial. Author is the recommended
55
			# keyword in the PNG spec. However some people output
56
			# Artist so support both.
57
			'artist' => 'Artist',
58
			'model' => 'Model',
59
			'make' => 'Make',
60
			'author' => 'Artist',
61
			'comment' => 'PNGFileComment',
62
			'description' => 'ImageDescription',
63
			'title' => 'ObjectName',
64
			'copyright' => 'Copyright',
65
			# Source as in original device used to make image
66
			# not as in who gave you the image
67
			'source' => 'Model',
68
			'software' => 'Software',
69
			'disclaimer' => 'Disclaimer',
70
			'warning' => 'ContentWarning',
71
			'url' => 'Identifier', # Not sure if this is best mapping. Maybe WebStatement.
72
			'label' => 'Label',
73
			'creation time' => 'DateTimeDigitized',
74
			/* Other potentially useful things - Document */
75
		];
76
77
		$frameCount = 0;
78
		$loopCount = 1;
79
		$text = [];
80
		$duration = 0.0;
81
		$bitDepth = 0;
82
		$colorType = 'unknown';
83
84 View Code Duplication
		if ( !$filename ) {
85
			throw new Exception( __METHOD__ . ": No file name specified" );
86
		} elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
87
			throw new Exception( __METHOD__ . ": File $filename does not exist" );
88
		}
89
90
		$fh = fopen( $filename, 'rb' );
91
92
		if ( !$fh ) {
93
			throw new Exception( __METHOD__ . ": Unable to open file $filename" );
94
		}
95
96
		// Check for the PNG header
97
		$buf = fread( $fh, 8 );
98
		if ( $buf != self::$pngSig ) {
99
			throw new Exception( __METHOD__ . ": Not a valid PNG file; header: $buf" );
100
		}
101
102
		// Read chunks
103
		while ( !feof( $fh ) ) {
104
			$buf = fread( $fh, 4 );
105
			if ( !$buf || strlen( $buf ) < 4 ) {
106
				throw new Exception( __METHOD__ . ": Read error" );
107
			}
108
			$chunk_size = unpack( "N", $buf )[1];
109
110
			if ( $chunk_size < 0 ) {
111
				throw new Exception( __METHOD__ . ": Chunk size too big for unpack" );
112
			}
113
114
			$chunk_type = fread( $fh, 4 );
115
			if ( !$chunk_type || strlen( $chunk_type ) < 4 ) {
116
				throw new Exception( __METHOD__ . ": Read error" );
117
			}
118
119
			if ( $chunk_type == "IHDR" ) {
120
				$buf = self::read( $fh, $chunk_size );
121
				if ( !$buf || strlen( $buf ) < $chunk_size ) {
122
					throw new Exception( __METHOD__ . ": Read error" );
123
				}
124
				$bitDepth = ord( substr( $buf, 8, 1 ) );
125
				// Detect the color type in British English as per the spec
126
				// https://www.w3.org/TR/PNG/#11IHDR
127
				switch ( ord( substr( $buf, 9, 1 ) ) ) {
128
					case 0:
129
						$colorType = 'greyscale';
130
						break;
131
					case 2:
132
						$colorType = 'truecolour';
133
						break;
134
					case 3:
135
						$colorType = 'index-coloured';
136
						break;
137
					case 4:
138
						$colorType = 'greyscale-alpha';
139
						break;
140
					case 6:
141
						$colorType = 'truecolour-alpha';
142
						break;
143
					default:
144
						$colorType = 'unknown';
145
						break;
146
				}
147
			} elseif ( $chunk_type == "acTL" ) {
148
				$buf = fread( $fh, $chunk_size );
149
				if ( !$buf || strlen( $buf ) < $chunk_size || $chunk_size < 4 ) {
150
					throw new Exception( __METHOD__ . ": Read error" );
151
				}
152
153
				$actl = unpack( "Nframes/Nplays", $buf );
154
				$frameCount = $actl['frames'];
155
				$loopCount = $actl['plays'];
156
			} elseif ( $chunk_type == "fcTL" ) {
157
				$buf = self::read( $fh, $chunk_size );
158
				if ( !$buf || strlen( $buf ) < $chunk_size ) {
159
					throw new Exception( __METHOD__ . ": Read error" );
160
				}
161
				$buf = substr( $buf, 20 );
162
				if ( strlen( $buf ) < 4 ) {
163
					throw new Exception( __METHOD__ . ": Read error" );
164
				}
165
166
				$fctldur = unpack( "ndelay_num/ndelay_den", $buf );
167
				if ( $fctldur['delay_den'] == 0 ) {
168
					$fctldur['delay_den'] = 100;
169
				}
170
				if ( $fctldur['delay_num'] ) {
171
					$duration += $fctldur['delay_num'] / $fctldur['delay_den'];
172
				}
173
			} elseif ( $chunk_type == "iTXt" ) {
174
				// Extracts iTXt chunks, uncompressing if necessary.
175
				$buf = self::read( $fh, $chunk_size );
176
				$items = [];
177
				if ( preg_match(
178
					'/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds',
179
					$buf, $items )
180
				) {
181
					/* $items[1] = text chunk name, $items[2] = compressed flag,
182
					 * $items[3] = lang code (or ""), $items[4]= compression type.
183
					 * $items[5] = content
184
					 */
185
186
					// Theoretically should be case-sensitive, but in practise...
187
					$items[1] = strtolower( $items[1] );
188
					if ( !isset( self::$textChunks[$items[1]] ) ) {
189
						// Only extract textual chunks on our list.
190
						fseek( $fh, self::$crcSize, SEEK_CUR );
191
						continue;
192
					}
193
194
					$items[3] = strtolower( $items[3] );
195
					if ( $items[3] == '' ) {
196
						// if no lang specified use x-default like in xmp.
197
						$items[3] = 'x-default';
198
					}
199
200
					// if compressed
201
					if ( $items[2] == "\x01" ) {
202
						if ( function_exists( 'gzuncompress' ) && $items[4] === "\x00" ) {
203
							MediaWiki\suppressWarnings();
204
							$items[5] = gzuncompress( $items[5] );
205
							MediaWiki\restoreWarnings();
206
207 View Code Duplication
							if ( $items[5] === false ) {
208
								// decompression failed
209
								wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n" );
210
								fseek( $fh, self::$crcSize, SEEK_CUR );
211
								continue;
212
							}
213 View Code Duplication
						} else {
214
							wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,'
215
								. " or potentially invalid compression method\n" );
216
							fseek( $fh, self::$crcSize, SEEK_CUR );
217
							continue;
218
						}
219
					}
220
					$finalKeyword = self::$textChunks[$items[1]];
221
					$text[$finalKeyword][$items[3]] = $items[5];
222
					$text[$finalKeyword]['_type'] = 'lang';
223
				} else {
224
					// Error reading iTXt chunk
225
					throw new Exception( __METHOD__ . ": Read error on iTXt chunk" );
226
				}
227
			} elseif ( $chunk_type == 'tEXt' ) {
228
				$buf = self::read( $fh, $chunk_size );
229
230
				// In case there is no \x00 which will make explode fail.
231
				if ( strpos( $buf, "\x00" ) === false ) {
232
					throw new Exception( __METHOD__ . ": Read error on tEXt chunk" );
233
				}
234
235
				list( $keyword, $content ) = explode( "\x00", $buf, 2 );
236
				if ( $keyword === '' || $content === '' ) {
237
					throw new Exception( __METHOD__ . ": Read error on tEXt chunk" );
238
				}
239
240
				// Theoretically should be case-sensitive, but in practise...
241
				$keyword = strtolower( $keyword );
242
				if ( !isset( self::$textChunks[$keyword] ) ) {
243
					// Don't recognize chunk, so skip.
244
					fseek( $fh, self::$crcSize, SEEK_CUR );
245
					continue;
246
				}
247
				MediaWiki\suppressWarnings();
248
				$content = iconv( 'ISO-8859-1', 'UTF-8', $content );
249
				MediaWiki\restoreWarnings();
250
251
				if ( $content === false ) {
252
					throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
253
				}
254
255
				$finalKeyword = self::$textChunks[$keyword];
256
				$text[$finalKeyword]['x-default'] = $content;
257
				$text[$finalKeyword]['_type'] = 'lang';
258
			} elseif ( $chunk_type == 'zTXt' ) {
259
				if ( function_exists( 'gzuncompress' ) ) {
260
					$buf = self::read( $fh, $chunk_size );
261
262
					// In case there is no \x00 which will make explode fail.
263
					if ( strpos( $buf, "\x00" ) === false ) {
264
						throw new Exception( __METHOD__ . ": Read error on zTXt chunk" );
265
					}
266
267
					list( $keyword, $postKeyword ) = explode( "\x00", $buf, 2 );
268
					if ( $keyword === '' || $postKeyword === '' ) {
269
						throw new Exception( __METHOD__ . ": Read error on zTXt chunk" );
270
					}
271
					// Theoretically should be case-sensitive, but in practise...
272
					$keyword = strtolower( $keyword );
273
274
					if ( !isset( self::$textChunks[$keyword] ) ) {
275
						// Don't recognize chunk, so skip.
276
						fseek( $fh, self::$crcSize, SEEK_CUR );
277
						continue;
278
					}
279
					$compression = substr( $postKeyword, 0, 1 );
280
					$content = substr( $postKeyword, 1 );
281 View Code Duplication
					if ( $compression !== "\x00" ) {
282
						wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping.\n" );
283
						fseek( $fh, self::$crcSize, SEEK_CUR );
284
						continue;
285
					}
286
287
					MediaWiki\suppressWarnings();
288
					$content = gzuncompress( $content );
289
					MediaWiki\restoreWarnings();
290
291 View Code Duplication
					if ( $content === false ) {
292
						// decompression failed
293
						wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n" );
294
						fseek( $fh, self::$crcSize, SEEK_CUR );
295
						continue;
296
					}
297
298
					MediaWiki\suppressWarnings();
299
					$content = iconv( 'ISO-8859-1', 'UTF-8', $content );
300
					MediaWiki\restoreWarnings();
301
302
					if ( $content === false ) {
303
						throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
304
					}
305
306
					$finalKeyword = self::$textChunks[$keyword];
307
					$text[$finalKeyword]['x-default'] = $content;
308
					$text[$finalKeyword]['_type'] = 'lang';
309
				} else {
310
					wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping.\n" );
311
					fseek( $fh, $chunk_size, SEEK_CUR );
312
				}
313
			} elseif ( $chunk_type == 'tIME' ) {
314
				// last mod timestamp.
315
				if ( $chunk_size !== 7 ) {
316
					throw new Exception( __METHOD__ . ": tIME wrong size" );
317
				}
318
				$buf = self::read( $fh, $chunk_size );
319
				if ( !$buf || strlen( $buf ) < $chunk_size ) {
320
					throw new Exception( __METHOD__ . ": Read error" );
321
				}
322
323
				// Note: spec says this should be UTC.
324
				$t = unpack( "ny/Cm/Cd/Ch/Cmin/Cs", $buf );
325
				$strTime = sprintf( "%04d%02d%02d%02d%02d%02d",
326
					$t['y'], $t['m'], $t['d'], $t['h'],
327
					$t['min'], $t['s'] );
328
329
				$exifTime = wfTimestamp( TS_EXIF, $strTime );
330
331
				if ( $exifTime ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exifTime of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
332
					$text['DateTime'] = $exifTime;
333
				}
334
			} elseif ( $chunk_type == 'pHYs' ) {
335
				// how big pixels are (dots per meter).
336
				if ( $chunk_size !== 9 ) {
337
					throw new Exception( __METHOD__ . ": pHYs wrong size" );
338
				}
339
340
				$buf = self::read( $fh, $chunk_size );
341
				if ( !$buf || strlen( $buf ) < $chunk_size ) {
342
					throw new Exception( __METHOD__ . ": Read error" );
343
				}
344
345
				$dim = unpack( "Nwidth/Nheight/Cunit", $buf );
346
				if ( $dim['unit'] == 1 ) {
347
					// Need to check for negative because php
348
					// doesn't deal with super-large unsigned 32-bit ints well
349
					if ( $dim['width'] > 0 && $dim['height'] > 0 ) {
350
						// unit is meters
351
						// (as opposed to 0 = undefined )
352
						$text['XResolution'] = $dim['width']
353
							. '/100';
354
						$text['YResolution'] = $dim['height']
355
							. '/100';
356
						$text['ResolutionUnit'] = 3;
357
						// 3 = dots per cm (from Exif).
358
					}
359
				}
360
			} elseif ( $chunk_type == "IEND" ) {
361
				break;
362
			} else {
363
				fseek( $fh, $chunk_size, SEEK_CUR );
364
			}
365
			fseek( $fh, self::$crcSize, SEEK_CUR );
366
		}
367
		fclose( $fh );
368
369
		if ( $loopCount > 1 ) {
370
			$duration *= $loopCount;
371
		}
372
373
		if ( isset( $text['DateTimeDigitized'] ) ) {
374
			// Convert date format from rfc2822 to exif.
375
			foreach ( $text['DateTimeDigitized'] as $name => &$value ) {
376
				if ( $name === '_type' ) {
377
					continue;
378
				}
379
380
				// @todo FIXME: Currently timezones are ignored.
381
				// possibly should be wfTimestamp's
382
				// responsibility. (at least for numeric TZ)
383
				$formatted = wfTimestamp( TS_EXIF, $value );
384
				if ( $formatted ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $formatted of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
385
					// Only change if we could convert the
386
					// date.
387
					// The png standard says it should be
388
					// in rfc2822 format, but not required.
389
					// In general for the exif stuff we
390
					// prettify the date if we can, but we
391
					// display as-is if we cannot or if
392
					// it is invalid.
393
					// So do the same here.
394
395
					$value = $formatted;
396
				}
397
			}
398
		}
399
400
		return [
401
			'frameCount' => $frameCount,
402
			'loopCount' => $loopCount,
403
			'duration' => $duration,
404
			'text' => $text,
405
			'bitDepth' => $bitDepth,
406
			'colorType' => $colorType,
407
		];
408
	}
409
410
	/**
411
	 * Read a chunk, checking to make sure its not too big.
412
	 *
413
	 * @param resource $fh The file handle
414
	 * @param int $size Size in bytes.
415
	 * @throws Exception If too big
416
	 * @return string The chunk.
417
	 */
418
	private static function read( $fh, $size ) {
419
		if ( $size > self::MAX_CHUNK_SIZE ) {
420
			throw new Exception( __METHOD__ . ': Chunk size of ' . $size .
421
				' too big. Max size is: ' . self::MAX_CHUNK_SIZE );
422
		}
423
424
		return fread( $fh, $size );
425
	}
426
}
427