Edit in GitHubLog an issue

File uploads

When working with files, especially user-uploaded files, it is easy to make a mistake and open your store to dangerous attacks like path traversal and remote code execution (RCE). The Adobe Commerce and Magento Open Source framework provides abstraction to help you safely work with user files, but it's your responsibility to use it the right way.

When you don't need a file#

There are cases when users can upload files for their own convenience. For example, consider functionality that allows a customer to upload a .csv file with a list of SKUs and quantities to add products to their cart. You don't need to store the file, you only need the contents of the file to add those SKUs to a cart. One option is to read the uploaded file, add SKUs, and delete it without ever moving it from the temporary folder on the file system. Another, even better option for security and performance, is to never upload the file in the first place. The file can be handled on the frontend side using JavaScript to extract SKUs and quantities and send those to a web API endpoint on the server.

Files inaccessible by users#

Some files, generated or uploaded, need to be stored on the server for further processing or querying, but should not be directly accessible through a URL. Below are measures to avoid potential unauthorized access, path traversal, or RCE problems from such files:

  • Use random file names and extensions (it's better to use no file extensions); do not trust file names provided by users
  • Store files in a directory specifically for generated/uploaded files
  • Do not store these files in an HTTP accessible folder (like /pub)
  • Store file records in a database if the files need to be assigned to an entity
  • Do not trust user provided file names/IDs when deleting files; validate file ownership through the database

The Magento\Framework\Filesystem class can help you find the right folder to store the files. Usually, generated or inaccessible files are stored in the /var directory. See the following examples:

Copied to your clipboard
1class MyClass {
2 private \Magento\Framework\Filesystem $filesystem;
3
4 private \Magento\Framework\Filesystem\Directory\WriteFactory $writeFactory;
5
6 private \Magento\Framework\Math\Random $rand;
7
8 public function __construct(
9 \Magento\Framework\Filesystem $filesystem,
10 \Magento\Framework\Filesystem\Directory\WriteFactory $writeFactory,
11 \Magento\Framework\Math\Random $rand
12 ) {
13 $this->filesystem = $filesystem;
14 $this->writeFactory = $writeFactory;
15 $this->rand = $rand;
16 }
17
18 ...
19
20 public function workWithFiles(): void {
21 ...
22
23 //To read "MAGENTO_ROOT/var" sub-directories or files.
24 $varDir = $this->filesystem->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR);
25 //Going to write files into a designated folder specific to these type of files and functionality
26 //Getting WriteInterface instance of `MAGENTO_ROOT/var/my-modules-dir`
27 $thisModulesFilesDir = $this->writeFactory->create($varDir->getAbsolutePath('my-modules-dir'));
28
29 //Random file name
30 $randomFileName = $this->rand->getRandomString(32);
31 //Copying a file from the system temporary directory into it's new path
32 $thisModulesFilesDir->getDriver()
33 ->copy($tmpUploadedOrGeneratedFilePath, $thisModulesFilesDir->getAbsolutePath($randomFileName));
34 }
35}

Files that require authorization#

You should treat files that require authorization to download the same way as inaccessible files; with a controller that performs authorization and then serves the file by outputting its content in response body.

Publicly accessible media files#

Publicly accessible media files present higher risk and require special care because you must keep the user-provided path and file extension. You should verify the following:

  • Media files can only be placed in a publicly accessible path
  • Uploaded file path is inside the designated folder or its subdirectories
  • Extension is safe (use an allow-list)
  • File path is out of system folders that contain other application files
  • Prevent deleting system files in public folders
  • Ideally, verify user's relation to file (ownership), or containing directory before updating or deleting files

Notes:

  • The application uses the \Magento\Framework\App\Filesystem\DirectoryList::PUB directory for public files.
  • Uploaded file paths must be validated using the ReadInterface and WriteInterface instances, similar to the preceding example.
  • \Magento\Framework\Filesystem\Io\File can help extract file extensions from filenames.

Example of an imaginary class dealing with media files:

Copied to your clipboard
1class MyFileUploader {
2 private const UPLOAD_DIR = 'my-module/customer-jpegs';
3
4 private \Magento\Framework\Filesystem\Io\File $fileUtil;
5
6 private array $allowedExt = ['jpg', 'jpeg'];
7
8 private \Magento\Framework\Filesystem\Directory\WriteFactory $writeFactory;
9
10 private \Magento\Framework\Filesystem $filesystem;
11
12 /**
13 * @param string $customerId UserContextInterface::getUserId() - current customer
14 * @param array $uploadedFileData uploaded file data from $_FILES
15 * @return MediaFile
16 * @throws \Magento\Framework\Exception\ValidatorException
17 */
18 public function upload(string $customerId, array $uploadedFileData): MediaFile
19 {
20 //Get upload file's metadata
21 $info = $this->fileUtil->getPathInfo($uploadedFileData['name']);
22 //Validate extension is allowed
23 if (!in_array($info['extension'], $this->allowedExt, true)) {
24 throw new ValidationException('Only JPEG files allowed');
25 }
26
27 //Initiate WriteInterface instance of the target directory
28 //Target dir is a sub-dir of PUB
29 $uploadDir = $this->writeFactory->create(
30 $this->filesystem->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::PUB)
31 ->getAbsolutePath(self::UPLOAD_DIR)
32 );
33 //Get target path if uploaded to the dir
34 $realPath =$uploadDir->getDriver()->getRealPathSafety($uploadDir->getAbsolutePath($uploadedFileData['name']));
35
36 //Validate that the target file name is not a system file
37 $this->validateNotSystemFile($realPath);
38 //Validate that target folder (UPLOAD_DIR + ['name'] - ['basename']) is not a system folder
39 $this->validateNotSystemFolder(preg_replace('/\/[^\/]+$/', '', $realPath));
40 //Validate that given file doesn't exist or is own by current customer
41 $existingMediaFileInfo = $this->findFileByRelativePath($realPath);
42 if ($existingMediaFileInfo && $existingMediaFileInfo->getCustomerId() !== $customerId) {
43 throw new ValidationException('Access denied');
44 }
45
46 //Copy temp file to target path
47 $uploadDir->getDriver()->copy(
48 $uploadedFileData['tmp_name'],
49 $realPath
50 );
51
52 //Persist file info
53 $mediaFile = new MediaFile($customerId, $realPath);
54 return $this->persist($mediaFile);
55 }
56}
Was this helpful?
  • Privacy
  • Terms of Use
  • Do not sell my personal information
  • AdChoices
Copyright © 2022 Adobe. All rights reserved.