Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions ext/fileinfo/fileinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ PHP_FUNCTION(finfo_set_flags)
}
/* }}} */

static const char* php_fileinfo_from_path(struct magic_set *magic, const zend_string *path, php_stream_context *context)
static const char* php_fileinfo_from_path(struct magic_set *magic, const zval *self, const zend_string *path, php_stream_context *context)
{
ZEND_ASSERT(magic != NULL);
ZEND_ASSERT((magic != NULL) ^ (self != NULL));
ZEND_ASSERT(path);
ZEND_ASSERT(ZSTR_LEN(path) != 0);
ZEND_ASSERT(!zend_str_has_nul_byte(path));
Expand Down Expand Up @@ -266,6 +266,9 @@ static const char* php_fileinfo_from_path(struct magic_set *magic, const zend_st
}
}
if (!ret_val) {
if (magic == NULL) {
magic = Z_FINFO_P(self)->magic;
}
ret_val = magic_stream(magic, stream);
if (UNEXPECTED(ret_val == NULL)) {
php_error_docref(NULL, E_WARNING, "Failed identify data %d:%s", magic_errno(magic), magic_error(magic));
Expand Down Expand Up @@ -313,7 +316,13 @@ PHP_FUNCTION(finfo_file)
magic_setflags(magic, options);
}

const char *ret_val = php_fileinfo_from_path(magic, path, context);
const char *ret_val = php_fileinfo_from_path(NULL, self, path, context);

if (!Z_FINFO_P(self)->magic) {
zend_throw_error(NULL, "Invalid finfo object");
RETURN_THROWS();
}
magic = Z_FINFO_P(self)->magic;

/* Restore options */
if (options) {
Expand Down Expand Up @@ -425,7 +434,7 @@ PHP_FUNCTION(mime_content_type)
const char *ret_val;
if (path) {
php_stream_context *context = php_stream_context_get_default(false);
ret_val = php_fileinfo_from_path(magic, path, context);
ret_val = php_fileinfo_from_path(magic, NULL, path, context);
} else {
/* remember stream position for restoration */
zend_off_t current_stream_pos = php_stream_tell(stream);
Expand Down
79 changes: 79 additions & 0 deletions ext/fileinfo/tests/fileinfo-controlled-free.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
--TEST--
Controlled free with re-entrant file info constructor
--EXTENSIONS--
fileinfo
--CREDITS--
Calif.io
--FILE--
<?php

final class AuditFileinfoControlledStream
{
public $context;
private int $offset = 0;
private static bool $reentered = false;

public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
{
if (!self::$reentered) {
self::$reentered = true;
echo "stream-open-release\n";
try {
$GLOBALS['finfo']->__construct(
FILEINFO_MIME_TYPE,
dirname(__DIR__) . '/fixtures/does-not-exist.magic',
);
} catch (Throwable $error) {
echo "constructor-failed\n";
}

for ($i = 0; $i < 128; ++$i) {
$GLOBALS['spray'][] = str_repeat('A', 279);
}
echo "same-bin-spray-complete\n";
}
return true;
}

public function stream_stat(): false
{
return false;
}

public function url_stat(string $path, int $flags): array|false {
return false;
}

public function stream_read(int $count): string
{
$data = "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n";
$chunk = substr($data, $this->offset, $count);
$this->offset += strlen($chunk);
return $chunk;
}

public function stream_eof(): bool
{
return $this->offset >= 32;
}
}

stream_wrapper_register('auditcontrolled', AuditFileinfoControlledStream::class);

$spray = [];
$finfo = new finfo(FILEINFO_MIME_TYPE);
var_dump($finfo->file('auditcontrolled://attacker-controlled-upload'));

?>
--EXPECTF--
stream-open-release
constructor-failed
same-bin-spray-complete

Warning: finfo::file(): Failed identify data 22:Magic database is not open in %s on line %d

Fatal error: Uncaught Error: Invalid finfo object in %s:%d
Stack trace:
#0 %s(%d): finfo->file('auditcontrolled...')
#1 {main}
thrown in %s on line %d
67 changes: 67 additions & 0 deletions ext/fileinfo/tests/fileinfo-http-notification-reconstruct-uaf.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
--TEST--
Fileinfo reconstruction UAF with re-entrant file info constructor (HTTP notification)
--EXTENSIONS--
fileinfo
--CREDITS--
Calif.io
--SKIPIF--
<?php
if (!file_exists(__DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc")) {
echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
}
?>
--FILE--
<?php

include __DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc";

$reentered = false;
$spray = [];
$finfo = new finfo(FILEINFO_MIME_TYPE);

$context = stream_context_create([], [
'notification' => static function (
int $notificationCode,
int $severity,
?string $message,
int $messageCode,
int $bytesTransferred,
int $bytesMax,
) use (&$reentered, &$spray, $finfo): void {
if ($reentered) {
return;
}
$reentered = true;
echo "http-notification-reconstruct:$notificationCode\n";
try {
$finfo->__construct(
FILEINFO_MIME_TYPE,
dirname(__DIR__) . '/fixtures/does-not-exist.magic',
);
} catch (Throwable $error) {
echo "constructor-failed\n";
}
for ($i = 0; $i < 128; ++$i) {
$spray[] = str_repeat('A', 279);
}
echo "same-bin-spray-complete\n";
},
]);

php_cli_server_start();
$url = "http://" . PHP_CLI_SERVER_ADDRESS;
var_dump($finfo->file($url, 0, $context));

?>
--EXPECTF--
http-notification-reconstruct:2
constructor-failed
same-bin-spray-complete

Warning: finfo::file(): Failed identify data 22:Magic database is not open in %s on line %d

Fatal error: Uncaught Error: Invalid finfo object in %s:%d
Stack trace:
#0 %s(%d): finfo->file('http://localhos...', 0, Resource id #%d)
#1 {main}
thrown in %s on line %d
62 changes: 62 additions & 0 deletions ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
Fileinfo reconstruction UAF with re-entrant file info constructor (stream stat)
--EXTENSIONS--
fileinfo
--CREDITS--
Calif.io
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php

final class AuditFileinfoStream
{
public $context;
private int $offset = 0;
private static bool $reentered = false;

public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
{
if (!self::$reentered) {
self::$reentered = true;
echo "stream-open-reconstruct\n";
$GLOBALS['finfo']->__construct(FILEINFO_MIME_TYPE);
}
return true;
}

public function stream_stat(): false
{
return false;
}

public function url_stat(string $path, int $flags): array|false {
return false;
}

public function stream_read(int $count): string
{
$data = "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n";
$chunk = substr($data, $this->offset, $count);
$this->offset += strlen($chunk);
return $chunk;
}

public function stream_eof(): bool
{
return $this->offset >= 32;
}

}

stream_wrapper_register('auditfinfo', AuditFileinfoStream::class);

$finfo = new finfo(FILEINFO_MIME_TYPE);
var_dump($finfo->file('auditfinfo://attacker-controlled-upload'));

?>
--EXPECTF--
stream-open-reconstruct

Warning: finfo::file(): AuditFileinfoStream::stream_cast is not implemented! in %s on line %d
string(15) "application/pdf"
Loading