Commit 1994ca5c authored by xjm's avatar xjm

SA-CORE-2020-012 by ufku, mrf, fgm, samuel.mortenson, dww, Heine, mlhess,...

SA-CORE-2020-012 by ufku, mrf, fgm, samuel.mortenson, dww, Heine, mlhess, David_Rothstein, pwolanin, xjm, fgm, stefan.r, dsnopek, rickmanelius, David Strauss, tedbow, alexpott, dww, larowlan, kim.pepper, Wim Leers, quicksketch, mcdruid, Fabianx, effulgentsia, drumm, pandaski, Mixologic
parent d336fce6
......@@ -159,8 +159,8 @@ function file_build_uri($path) {
* exploit.php_.pps.
*
* Specifically, this function adds an underscore to all extensions that are
* between 2 and 5 characters in length, internal to the file name, and not
* included in $extensions.
* between 2 and 5 characters in length, internal to the file name, and either
* included in the list of unsafe extensions, or not included in $extensions.
*
* Function behavior is also controlled by the configuration
* 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
......@@ -168,7 +168,8 @@ function file_build_uri($path) {
* @param $filename
* File name to modify.
* @param $extensions
* A space-separated list of extensions that should not be altered.
* A space-separated list of extensions that should not be altered. Note that
* extensions that are unsafe will be altered regardless of this parameter.
* @param $alerts
* If TRUE, \Drupal::messenger()->addStatus() will be called to display
* a message if the file name was changed.
......@@ -187,6 +188,12 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
$allowed_extensions = array_unique(explode(' ', strtolower(trim($extensions))));
// Remove unsafe extensions from the allowed list of extensions.
// @todo https://www.drupal.org/project/drupal/issues/3032390 Make the list
// of unsafe extensions a constant. The list is copied from
// FILE_INSECURE_EXTENSION_REGEX.
$allowed_extensions = array_diff($allowed_extensions, explode('|', 'phar|php|pl|py|cgi|asp|js'));
// Split the filename up by periods. The first part becomes the basename
// the last part the final extension.
$filename_parts = explode('.', $filename);
......
......@@ -281,7 +281,17 @@ function file_validate(FileInterface $file, $validators = []) {
}
// Let other modules perform validation on the new file.
return array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
$errors = array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
// Ensure the file does not contain a malicious extension. At this point
// _file_save_upload_single() will have munged the file so it does not contain
// a malicious extension. Contributed and custom code that calls this method
// needs to take similar steps if they need to permit files with malicious
// extensions to be uploaded.
if (empty($errors) && !\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())) {
$errors[] = t('For security reasons, your upload has been rejected.');
}
return $errors;
}
/**
......@@ -978,25 +988,36 @@ function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $va
$validators['file_validate_extensions'][0] = $extensions;
}
if (!empty($extensions)) {
// Munge the filename to protect against possible malicious extension
// hiding within an unknown file type (ie: filename.html.foo).
$file->setFilename(file_munge_filename($file->getFilename(), $extensions));
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
// The destination filename will also later be used to create the URI.
$file->setFilename($file->getFilename() . '.txt');
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
// Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
if (!empty($extensions)) {
$validators['file_validate_extensions'][0] .= ' txt';
\Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
// Munge the filename to protect against possible malicious extension
// hiding within an unknown file type (ie: filename.html.foo).
$file->setFilename(file_munge_filename($file->getFilename(), $extensions));
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php_.foo_.txt and
// filename.php_.txt, respectively).
if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())) {
// If the file will be rejected anyway due to a disallowed extension, it
// should not be renamed; rather, we'll let file_validate_extensions()
// reject it below.
if (!isset($validators['file_validate_extensions']) || empty(file_validate_extensions($file, $extensions))) {
$file->setMimeType('text/plain');
$filename = $file->getFilename();
if (substr($filename, -4) != '.txt') {
// The destination filename will also later be used to create the URI.
$filename .= '.txt';
}
$file->setFilename(file_munge_filename($filename, $extensions));
\Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
// The .txt extension may not be in the allowed list of extensions. We
// have to add it here or else the file upload will fail.
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
}
}
}
}
......
......@@ -468,26 +468,42 @@ protected function validate(FileInterface $file, array $validators) {
* The prepared/munged filename.
*/
protected function prepareFilename($filename, array &$validators) {
if (!empty($validators['file_validate_extensions'][0])) {
// If there is a file_validate_extensions validator and a list of
// valid extensions, munge the filename to protect against possible
// malicious extension hiding within an unknown file type. For example,
// "filename.html.foo".
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
// The destination filename will also later be used to create the URI.
$filename .= '.txt';
// The .txt extension may not be in the allowed list of extensions. We
// have to add it here or else the file upload will fail.
// Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
if (!$this->systemFileConfig->get('allow_insecure_uploads')) {
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
// If there is a file_validate_extensions validator and a list of
// valid extensions, munge the filename to protect against possible
// malicious extension hiding within an unknown file type. For example,
// "filename.html.foo".
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
}
// Rename potentially executable files, to help prevent exploits (i.e.
// will rename filename.php.foo and filename.php to filename._php._foo.txt
// and filename._php.txt, respectively).
if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename)) {
// If the file will be rejected anyway due to a disallowed extension, it
// should not be renamed; rather, we'll let file_validate_extensions()
// reject it below.
$passes_validation = FALSE;
if (!empty($validators['file_validate_extensions'][0])) {
$file = File::create([]);
$file->setFilename($filename);
$passes_validation = empty(file_validate_extensions($file, $validators['file_validate_extensions'][0]));
}
if (empty($validators['file_validate_extensions'][0]) || $passes_validation) {
if ((substr($filename, -4) != '.txt')) {
// The destination filename will also later be used to create the URI.
$filename .= '.txt';
}
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0] ?? '');
// The .txt extension may not be in the allowed list of extensions. We
// have to add it here or else the file upload will fail.
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
}
}
}
}
......
......@@ -49,9 +49,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
];
$form['allow_all_extensions'] = [
'#type' => 'checkbox',
'#title' => t('Allow all extensions?'),
'#default_value' => FALSE,
'#type' => 'radios',
'#options' => [
'false' => 'No',
'empty_array' => 'Empty array',
'empty_string' => 'Empty string',
],
'#default_value' => 'false',
];
$form['is_image_file'] = [
......@@ -92,9 +97,13 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$validators['file_validate_is_image'] = [];
}
if ($form_state->getValue('allow_all_extensions')) {
$allow = $form_state->getValue('allow_all_extensions');
if ($allow === 'empty_array') {
$validators['file_validate_extensions'] = [];
}
elseif ($allow === 'empty_string') {
$validators['file_validate_extensions'] = [''];
}
elseif (!$form_state->isValueEmpty('extensions')) {
$validators['file_validate_extensions'] = [$form_state->getValue('extensions')];
}
......
......@@ -90,9 +90,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
];
$form['allow_all_extensions'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow all extensions?'),
'#default_value' => FALSE,
'#title' => t('Allow all extensions?'),
'#type' => 'radios',
'#options' => [
'false' => 'No',
'empty_array' => 'Empty array',
'empty_string' => 'Empty string',
],
'#default_value' => 'false',
];
$form['is_image_file'] = [
......@@ -139,9 +144,13 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
$validators['file_validate_is_image'] = [];
}
if ($form_state->getValue('allow_all_extensions')) {
$allow = $form_state->getValue('allow_all_extensions');
if ($allow === 'empty_array') {
$validators['file_validate_extensions'] = [];
}
elseif ($allow === 'empty_string') {
$validators['file_validate_extensions'] = [''];
}
elseif (!$form_state->isValueEmpty('extensions')) {
$validators['file_validate_extensions'] = [$form_state->getValue('extensions')];
}
......
......@@ -198,7 +198,7 @@ public function testHandleExtension() {
$edit = [
'file_test_replace' => FileSystemInterface::EXISTS_REPLACE,
'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
'allow_all_extensions' => TRUE,
'allow_all_extensions' => 'empty_array',
];
$this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
$this->assertSession()->statusCodeEquals(200);
......@@ -227,7 +227,7 @@ public function testHandleDangerousFile() {
$this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
$this->assertSession()->statusCodeEquals(200);
$message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
$message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '_.txt' . '</em>';
$this->assertRaw($message);
$this->assertRaw(t('File MIME type is text/plain.'));
$this->assertRaw(t('You WIN!'));
......@@ -262,7 +262,8 @@ public function testHandleFileMunge() {
$file_system = \Drupal::service('file_system');
// Ensure insecure uploads are disabled for this test.
$this->config('system.file')->set('allow_insecure_uploads', 0)->save();
$this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);
$original_uri = $this->image->getFileUri();
$this->image = file_move($this->image, $original_uri . '.foo.' . $this->imageExtension);
// Reset the hook counters to get rid of the 'move' we just called.
file_test_reset();
......@@ -286,13 +287,37 @@ public function testHandleFileMunge() {
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
// Test with uppercase extensions.
$this->image = file_move($this->image, $original_uri . '.foo2.' . $this->imageExtension);
// Reset the hook counters.
file_test_reset();
$extensions = $this->imageExtension;
$edit = [
'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
'extensions' => mb_strtoupper($extensions),
];
$munged_filename = $this->image->getFilename();
$munged_filename = substr($munged_filename, 0, strrpos($munged_filename, '.'));
$munged_filename .= '_.' . $this->imageExtension;
$this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
$this->assertSession()->statusCodeEquals(200);
$this->assertRaw(t('For security reasons, your upload has been renamed'));
$this->assertRaw(t('File name is @filename', ['@filename' => $munged_filename]));
$this->assertRaw(t('You WIN!'));
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
// Ensure we don't munge files if we're allowing any extension.
// Reset the hook counters.
file_test_reset();
// Ensure we don't munge files if we're allowing any extension.
$edit = [
'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
'allow_all_extensions' => TRUE,
'allow_all_extensions' => 'empty_array',
];
$this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
......@@ -303,6 +328,24 @@ public function testHandleFileMunge() {
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
// Ensure that setting $validators['file_validate_extensions'] = ['']
// rejects all files.
// Reset the hook counters.
file_test_reset();
$edit = [
'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
'allow_all_extensions' => 'empty_string',
];
$this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
$this->assertSession()->statusCodeEquals(200);
$this->assertNoRaw(t('For security reasons, your upload has been renamed'));
$this->assertRaw(t('Epic upload FAIL!'));
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate']);
}
/**
......@@ -467,7 +510,6 @@ public function testCombinedErrorMessages() {
$submit_xpath = $this->assertSession()->buttonExists('Submit')->getXpath();
$form = $client->getCrawler()->filterXPath($submit_xpath)->form();
$edit = [
'allow_all_extensions' => FALSE,
'is_image_file' => TRUE,
'extensions' => 'jpeg',
];
......
......@@ -35,4 +35,23 @@ public function testCallerValidation() {
$this->assertFileHooksCalled(['validate']);
}
/**
* Tests hard-coded security check in file_validate().
*/
public function testInsecureExtensions() {
$file = $this->createFile('test.php', 'Invalid PHP');
// Test that file_validate() will check for insecure extensions by default.
$errors = file_validate($file, []);
$this->assertEquals('For security reasons, your upload has been rejected.', $errors[0]);
$this->assertFileHooksCalled(['validate']);
file_test_reset();
// Test that the 'allow_insecure_uploads' is respected.
$this->config('system.file')->set('allow_insecure_uploads', TRUE)->save();
$errors = file_validate($file, []);
$this->assertEmpty($errors);
$this->assertFileHooksCalled(['validate']);
}
}
......@@ -386,26 +386,42 @@ protected function validate(FileInterface $file, array $validators) {
* The prepared/munged filename.
*/
protected function prepareFilename($filename, array &$validators) {
if (!empty($validators['file_validate_extensions'][0])) {
// If there is a file_validate_extensions validator and a list of
// valid extensions, munge the filename to protect against possible
// malicious extension hiding within an unknown file type. For example,
// "filename.html.foo".
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
// The destination filename will also later be used to create the URI.
$filename .= '.txt';
// The .txt extension may not be in the allowed list of extensions. We
// have to add it here or else the file upload will fail.
// Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
if (!$this->systemFileConfig->get('allow_insecure_uploads')) {
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
// If there is a file_validate_extensions validator and a list of
// valid extensions, munge the filename to protect against possible
// malicious extension hiding within an unknown file type. For example,
// "filename.html.foo".
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
}
// Rename potentially executable files, to help prevent exploits (i.e.
// will rename filename.php.foo and filename.php to filename._php._foo.txt
// and filename._php.txt, respectively).
if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename)) {
// If the file will be rejected anyway due to a disallowed extension, it
// should not be renamed; rather, we'll let file_validate_extensions()
// reject it below.
$passes_validation = FALSE;
if (!empty($validators['file_validate_extensions'][0])) {
$file = File::create([]);
$file->setFilename($filename);
$passes_validation = empty(file_validate_extensions($file, $validators['file_validate_extensions'][0]));
}
if (empty($validators['file_validate_extensions'][0]) || $passes_validation) {
if (substr($filename, -4) != '.txt') {
// The destination filename will also later be used to create the URI.
$filename .= '.txt';
}
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0] ?? '');
// The .txt extension may not be in the allowed list of extensions. We
// have to add it here or else the file upload will fail.
if (!empty($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
}
}
}
}
......
......@@ -621,11 +621,11 @@ public function testFileUploadMaliciousExtension() {
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
// The filename is not munged because .txt is added and it is a known
// extension to apache.
$expected = $this->getExpectedDocument(1, 'example.php.txt', TRUE);
$expected = $this->getExpectedDocument(1, 'example.php_.txt', TRUE);
// Override the expected filesize.
$expected['data']['attributes']['filesize'] = strlen($php_string);
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example.php.txt');
$this->assertFileExists('public://foobar/example.php_.txt');
// Add php as an allowed format. Allow insecure uploads still being FALSE
// should still not allow this. So it should still have a .txt extension
......@@ -635,11 +635,11 @@ public function testFileUploadMaliciousExtension() {
$this->rebuildAll();
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
$expected = $this->getExpectedDocument(2, 'example_2.php.txt', TRUE);
$expected = $this->getExpectedDocument(2, 'example_2.php_.txt', TRUE);
// Override the expected filesize.
$expected['data']['attributes']['filesize'] = strlen($php_string);
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_2.php.txt');
$this->assertFileExists('public://foobar/example_2.php_.txt');
$this->assertFileNotExists('public://foobar/example_2.php');
// Allow .doc file uploads and ensure even a mis-configured apache will not
......@@ -659,6 +659,45 @@ public function testFileUploadMaliciousExtension() {
$this->assertFileExists('public://foobar/example_3.php_.doc');
$this->assertFileNotExists('public://foobar/example_3.php.doc');
// Test that a dangerous extension such as .php is munged even if it is in
// the list of allowed extensions.
$this->field->setSetting('file_extensions', 'doc php')->save();
$this->rebuildAll();
// Test using a masked exploit file.
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php.doc"']);
// The filename is munged.
$expected = $this->getExpectedDocument(4, 'example_4.php_.doc', TRUE);
// Override the expected filesize.
$expected['data']['attributes']['filesize'] = strlen($php_string);
// The file mime should be 'application/msword'.
$expected['data']['attributes']['filemime'] = 'application/msword';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_4.php_.doc');
$this->assertFileNotExists('public://foobar/example_4.php.doc');
// Dangerous extensions are munged even when all extensions are allowed.
$this->field->setSetting('file_extensions', '')->save();
$this->rebuildAll();
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_5.php.png"']);
$expected = $this->getExpectedDocument(5, 'example_5.php_.png_.txt', TRUE);
// Override the expected filesize.
$expected['data']['attributes']['filesize'] = strlen($php_string);
// The file mime should also now be text.
$expected['data']['attributes']['filemime'] = 'text/plain';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_5.php_.png_.txt');
// Dangerous extensions are munged if is renamed to end in .txt.
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_6.cgi.png.txt"']);
$expected = $this->getExpectedDocument(6, 'example_6.cgi_.png_.txt', TRUE);
// Override the expected filesize.
$expected['data']['attributes']['filesize'] = strlen($php_string);
// The file mime should also now be text.
$expected['data']['attributes']['filemime'] = 'text/plain';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_6.cgi_.png_.txt');
// Now allow insecure uploads.
\Drupal::configFactory()
->getEditable('system.file')
......@@ -668,14 +707,14 @@ public function testFileUploadMaliciousExtension() {
$this->field->setSetting('file_extensions', '')->save();
$this->rebuildAll();
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php"']);
$expected = $this->getExpectedDocument(4, 'example_4.php', TRUE);
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
$expected = $this->getExpectedDocument(7, 'example_7.php', TRUE);
// Override the expected filesize.
$expected['data']['attributes']['filesize'] = strlen($php_string);
// The file mime should also now be PHP.
$expected['data']['attributes']['filemime'] = 'application/x-httpd-php';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_4.php');
$this->assertFileExists('public://foobar/example_7.php');
}
/**
......
......@@ -517,11 +517,11 @@ public function testFileUploadMaliciousExtension() {
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
// The filename is not munged because .txt is added and it is a known
// extension to apache.
$expected = $this->getExpectedNormalizedEntity(1, 'example.php.txt', TRUE);
$expected = $this->getExpectedNormalizedEntity(1, 'example.php_.txt', TRUE);
// Override the expected filesize.
$expected['filesize'][0]['value'] = strlen($php_string);
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example.php.txt');
$this->assertFileExists('public://foobar/example.php_.txt');
// Add php as an allowed format. Allow insecure uploads still being FALSE
// should still not allow this. So it should still have a .txt extension
......@@ -531,11 +531,11 @@ public function testFileUploadMaliciousExtension() {
$this->refreshTestStateAfterRestConfigChange();
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
$expected = $this->getExpectedNormalizedEntity(2, 'example_2.php.txt', TRUE);
$expected = $this->getExpectedNormalizedEntity(2, 'example_2.php_.txt', TRUE);
// Override the expected filesize.
$expected['filesize'][0]['value'] = strlen($php_string);
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_2.php.txt');
$this->assertFileExists('public://foobar/example_2.php_.txt');
$this->assertFileNotExists('public://foobar/example_2.php');
// Allow .doc file uploads and ensure even a mis-configured apache will not
......@@ -555,6 +555,45 @@ public function testFileUploadMaliciousExtension() {
$this->assertFileExists('public://foobar/example_3.php_.doc');
$this->assertFileNotExists('public://foobar/example_3.php.doc');
// Test that a dangerous extension such as .php is munged even if it is in
// the list of allowed extensions.
$this->field->setSetting('file_extensions', 'doc php')->save();
$this->refreshTestStateAfterRestConfigChange();
// Test using a masked exploit file.
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php.doc"']);
// The filename is munged.
$expected = $this->getExpectedNormalizedEntity(4, 'example_4.php_.doc', TRUE);
// Override the expected filesize.
$expected['filesize'][0]['value'] = strlen($php_string);
// The file mime should be 'application/msword'.
$expected['filemime'][0]['value'] = 'application/msword';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_4.php_.doc');
$this->assertFileNotExists('public://foobar/example_4.php.doc');
// Dangerous extensions are munged even when all extensions are allowed.
$this->field->setSetting('file_extensions', '')->save();
$this->rebuildAll();
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_5.php.png"']);
$expected = $this->getExpectedNormalizedEntity(5, 'example_5.php_.png_.txt', TRUE);
// Override the expected filesize.
$expected['filesize'][0]['value'] = strlen($php_string);
// The file mime should also now be text.
$expected['filemime'][0]['value'] = 'text/plain';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_5.php_.png_.txt');
// Dangerous extensions are munged if is renamed to end in .txt.
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_6.cgi.png.txt"']);
$expected = $this->getExpectedNormalizedEntity(6, 'example_6.cgi_.png_.txt', TRUE);
// Override the expected filesize.
$expected['filesize'][0]['value'] = strlen($php_string);
// The file mime should also now be text.
$expected['filemime'][0]['value'] = 'text/plain';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_6.cgi_.png_.txt');
// Now allow insecure uploads.
\Drupal::configFactory()
->getEditable('system.file')
......@@ -564,14 +603,14 @@ public function testFileUploadMaliciousExtension() {
$this->field->setSetting('file_extensions', '')->save();
$this->refreshTestStateAfterRestConfigChange();
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php"']);
$expected = $this->getExpectedNormalizedEntity(4, 'example_4.php', TRUE);
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
$expected = $this->getExpectedNormalizedEntity(7, 'example_7.php', TRUE);
// Override the expected filesize.
$expected['filesize'][0]['value'] = strlen($php_string);
// The file mime should also now be PHP.
$expected['filemime'][0]['value'] = 'application/x-httpd-php';
$this->assertResponseData($expected, $response);
$this->assertFileExists('public://foobar/example_4.php');
$this->assertFileExists('public://foobar/example_7.php');
}