Skip to content
201 changes: 180 additions & 21 deletions app/code/Magento/Customer/Model/Metadata/Form/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,81 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request)
*/
protected function _validateByRules($value)
{
$label = $value['name'];
$rules = $this->getAttribute()->getValidationRules();
$extension = $this->ioFile->getPathInfo($value['name'])['extension'];
$fileExtensions = ArrayObjectSearch::getArrayElementByName(
$rules,
'file_extensions'
);
$label = $value['name'] ?? $value['file'] ?? '';
$rules = $this->getAttribute()->getValidationRules() ?? [];

// Extract and validate file name
$fileNameResult = $this->extractAndValidateFileName($value, $label);
if (!isset($fileNameResult['name'])) {
return $fileNameResult; // Return error array
}
$value['name'] = $fileNameResult['name'];
$label = $fileNameResult['label'];

// Validate file extension
$extensionResult = $this->validateFileExtension($value['name'], $rules, $label);
if (!empty($extensionResult)) {
return $extensionResult;
}

// Validate file path
$filePath = $this->getFilePath($value, $label);
if (is_array($filePath)) {
return $filePath; // Return error array
}

// Validate file size
return $this->validateFileSize($value, $rules, $label);
}

/**
* Extract and validate file name from value
*
* @param array $value
* @param string $label
* @return array Returns array with name and label or error array
*/
private function extractAndValidateFileName(array $value, string $label): array
{
// For UI component uploads, get name from file path if not provided
if (empty($value['name']) && !empty($value['file'])) {
// Validate file path for security before extracting filename
if (!$this->isValidFilePath($value['file'])) {
return [__('"%1" is not a valid file.', $label)];
}
$pathInfo = $this->ioFile->getPathInfo($value['file']);
$name = $pathInfo['basename'] ?? '';
$label = $name;
} else {
$name = $value['name'] ?? '';
}

// Ensure we have a valid file name
if (empty($name)) {
return [__('"%1" is not a valid file.', $label)];
}

return ['name' => $name, 'label' => $label];
}

/**
* Validate file extension
*
* @param string $fileName
* @param array $rules
* @param string $label
* @return array Returns error array or empty array
*/
private function validateFileExtension(string $fileName, array $rules, string $label): array
{
$pathInfo = $this->ioFile->getPathInfo($fileName);
$extension = $pathInfo['extension'] ?? '';

if (empty($extension)) {
return [__('"%1" is not a valid file.', $label)];
}

$fileExtensions = ArrayObjectSearch::getArrayElementByName($rules, 'file_extensions');
if ($fileExtensions !== null) {
$extensions = explode(',', $fileExtensions);
$extensions = array_map('trim', $extensions);
Expand All @@ -209,31 +277,121 @@ protected function _validateByRules($value)
}
}

/**
* Check protected file extension
*/
// Check protected file extension
if (!$this->_fileValidator->isValid($extension)) {
return $this->_fileValidator->getMessages();
}

if (!$this->_isUploadedFile($value['tmp_name'])) {
return [];
}

/**
* Get and validate file path
*
* @param array $value
* @param string $label
* @return string|array Returns file path or error array
*/
private function getFilePath(array $value, string $label)
{
$filePath = $value['tmp_name'] ?? $value['file'] ?? null;
if (empty($filePath)) {
return [__('"%1" is not a valid file.', $label)];
}

$maxFileSize = ArrayObjectSearch::getArrayElementByName(
$rules,
'max_file_size'
);
if ($maxFileSize !== null) {
$size = $value['size'];
if ($maxFileSize < $size) {
return [__('"%1" exceeds the allowed file size.', $label)];
}
if (!$this->_isUploadedFile($filePath)) {
return [__('"%1" is not a valid file.', $label)];
}

return $filePath;
}

/**
* Validate file size
*
* @param array $value
* @param array $rules
* @param string $label
* @return array Returns error array or empty array
*/
private function validateFileSize(array $value, array $rules, string $label): array
{
$maxFileSize = ArrayObjectSearch::getArrayElementByName($rules, 'max_file_size');
if ($maxFileSize === null) {
return [];
}

$size = $value['size'] ?? 0;
// For UI component uploads, get file size if not provided
if ($size === 0 && !empty($value['file'])) {
$size = $this->getTemporaryFileSize($value['file']);
}

if ($maxFileSize < $size) {
return [__('"%1" exceeds the allowed file size.', $label)];
}

return [];
}

/**
* Validate file path for security
*
* @param string $filePath
* @return bool
*/
private function isValidFilePath(string $filePath): bool
{
// Check for null bytes
if (strpos($filePath, "\0") !== false) {
return false;
}

// Check for path traversal sequences
if (preg_match('#(^|/)\.\.(?:/|$)#', $filePath)) {
return false;
}

// Check for Windows absolute paths
if (preg_match('#^[a-zA-Z]:[\\\\/]#', $filePath)) {
return false;
}

// Check for backslashes at the start
if (isset($filePath[0]) && $filePath[0] === '\\') {
return false;
}

return true;
}

/**
* Get file size from temporary directory
*
* @param string $filePath
* @return int
*/
private function getTemporaryFileSize(string $filePath): int
{
if (!$this->isValidFilePath($filePath)) {
return 0;
}

$pathInfo = $this->ioFile->getPathInfo($filePath);
$fileName = $pathInfo['basename'] ?? '';
if (empty($fileName)) {
return 0;
}

$temporaryFile = FileProcessor::TMP_DIR . '/' . ltrim($fileName, '/');
if ($this->fileProcessor->isExist($temporaryFile)) {
$stat = $this->fileProcessor->getStat($temporaryFile);
return (int)($stat['size'] ?? 0);
}

return 0;
}

/**
* Helper function that checks if the file was uploaded.
*
Expand Down Expand Up @@ -274,7 +432,8 @@ public function validateValue($value)
$label = $attribute->getStoreLabel();

$toDelete = !empty($value['delete']) ? true : false;
$toUpload = !empty($value['tmp_name']) ? true : false;
// Check both tmp_name (traditional upload) and file (UI component upload)
$toUpload = !empty($value['tmp_name']) || (!empty($value['file']) && $value['file'] !== $this->_value);

if (!$toUpload && !$toDelete && $this->_value) {
return true;
Expand Down
Loading