Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit 883f3b7

Browse files
Merge pull request magento-commerce/devdocs#2500 from AlexMaxHorkun/patch-7
Create security topic for uploaded files
2 parents 2c5c885 + 47b7ae1 commit 883f3b7

File tree

3 files changed

+157
-0
lines changed

3 files changed

+157
-0
lines changed

src/_data/toc/php-developer-guide.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ pages:
315315
- label: CSRF
316316
url: /extension-dev-guide/security/csrf.html
317317

318+
- label: Working with files
319+
url: /extension-dev-guide/security/uploads.html
320+
318321
- label: Versioning
319322
url: /extension-dev-guide/versioning/
320323
children:
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
group: php-developer-guide
3+
title: Working with files
4+
---
5+
6+
When working with files, especially user-uploaded files, it is easy to make a mistake and open your store to dangerous
7+
attacks like path traversal and remote code execution (RCE). The {{site.data.var.ee}} and {{site.data.var.ce}} framework provides abstraction to help you safely work with user files,
8+
but it's your responsibility to use it the right way.
9+
10+
## When you don't need a file
11+
There are cases when users can upload files for their own convenience. For example, consider functionality that allows
12+
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
13+
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
14+
SKUs, and delete it without ever moving it from the temporary folder on the file system. Another, even better option for security and
15+
performance, is to never upload the file in the first place. The file can be handled on the frontend side using JavaScript
16+
to extract SKUs and quantities and send those to a web API endpoint on the server.
17+
{.:bs-callout-tip}
18+
The best way to avoid security issues with files is to not upload or store them in the first place
19+
if you don't have to.
20+
21+
## Files inaccessible by users
22+
Some files, generated or uploaded, need to be stored on the server for further processing or querying, but should not be directly
23+
accessible through a URL. Below are measures to avoid potential unauthorized access, path traversal, or RCE problems
24+
from such files:
25+
26+
* Use random file names and extensions (it's better to use no file extensions); do not trust file names provided by users
27+
* Store files in a directory specifically for generated/uploaded files
28+
* Do not store these files in an HTTP accessible folder (like `/pub`)
29+
* Store file records in a database if the files need to be assigned to an entity
30+
* Do not trust user provided file names/IDs when deleting files; validate file ownership through the database
31+
32+
The `Magento\Framework\Filesystem` class can help you find the right folder to store the files. Usually,
33+
generated or inaccessible files are stored in the `/var` directory. See the following examples:
34+
35+
```php
36+
class MyClass {
37+
private \Magento\Framework\Filesystem $filesystem;
38+
39+
private \Magento\Framework\Filesystem\Directory\WriteFactory $writeFactory;
40+
41+
private \Magento\Framework\Math\Random $rand;
42+
43+
public function __construct(
44+
\Magento\Framework\Filesystem $filesystem,
45+
\Magento\Framework\Filesystem\Directory\WriteFactory $writeFactory,
46+
\Magento\Framework\Math\Random $rand
47+
) {
48+
$this->filesystem = $filesystem;
49+
$this->writeFactory = $writeFactory;
50+
$this->rand = $rand;
51+
}
52+
53+
...
54+
55+
public function workWithFiles(): void {
56+
...
57+
58+
//To read "MAGENTO_ROOT/var" sub-directories or files.
59+
$varDir = $this->filesystem->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR);
60+
//Going to write files into a designated folder specific to these type of files and functionality
61+
//Getting WriteInterface instance of `MAGENTO_ROOT/var/my-modules-dir`
62+
$thisModulesFilesDir = $this->writeFactory->create($varDir->getAbsolutePath('my-modules-dir'));
63+
64+
//Random file name
65+
$randomFileName = $this->rand->getRandomString(32);
66+
//Copying a file from the system temporary directory into it's new path
67+
$thisModulesFilesDir->getDriver()
68+
->copy($tmpUploadedOrGeneratedFilePath, $thisModulesFilesDir->getAbsolutePath($randomFileName));
69+
}
70+
}
71+
```
72+
73+
## Files that require authorization
74+
You should treat files that require authorization to download the same way as inaccessible files;
75+
with a controller that performs authorization and then serves the file by outputting its content in response body.
76+
77+
## Publicly accessible media files
78+
Publicly accessible media files present higher risk and require special care because you must keep the user-provided path
79+
and file extension. You should verify the following:
80+
81+
* Media files can only be placed in a publicly accessible path
82+
* Uploaded file path is inside the designated folder or its subdirectories
83+
* Extension is safe (use an allow-list)
84+
* File path is out of system folders that contain other application files
85+
* Prevent deleting system files in public folders
86+
* Ideally, verify user's relation to file (ownership), or containing directory before updating or deleting files
87+
88+
Notes:
89+
90+
* Magento uses the `\Magento\Framework\App\Filesystem\DirectoryList::PUB` directory for public files.
91+
* Uploaded file paths must be validated using the `ReadInterface` and `WriteInterface` instances, similar to the preceding example.
92+
* `\Magento\Framework\Filesystem\Io\File` can help extract file extensions from filenames.
93+
94+
Example of an imaginary class dealing with media files:
95+
96+
```php
97+
class MyFileUploader {
98+
private const UPLOAD_DIR = 'my-module/customer-jpegs';
99+
100+
private \Magento\Framework\Filesystem\Io\File $fileUtil;
101+
102+
private array $allowedExt = ['jpg', 'jpeg'];
103+
104+
private \Magento\Framework\Filesystem\Directory\WriteFactory $writeFactory;
105+
106+
private \Magento\Framework\Filesystem $filesystem;
107+
108+
/**
109+
* @param string $customerId UserContextInterface::getUserId() - current customer
110+
* @param array $uploadedFileData uploaded file data from $_FILES
111+
* @return MediaFile
112+
* @throws \Magento\Framework\Exception\ValidatorException
113+
*/
114+
public function upload(string $customerId, array $uploadedFileData): MediaFile
115+
{
116+
//Get upload file's metadata
117+
$info = $this->fileUtil->getPathInfo($uploadedFileData['name']);
118+
//Validate extension is allowed
119+
if (!in_array($info['extension'], $this->allowedExt, true)) {
120+
throw new ValidationException('Only JPEG files allowed');
121+
}
122+
123+
//Initiate WriteInterface instance of the target directory
124+
//Target dir is a sub-dir of PUB
125+
$uploadDir = $this->writeFactory->create(
126+
$this->filesystem->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::PUB)
127+
->getAbsolutePath(self::UPLOAD_DIR)
128+
);
129+
//Get target path if uploaded to the dir
130+
$realPath =$uploadDir->getDriver()->getRealPathSafety($uploadDir->getAbsolutePath($uploadedFileData['name']));
131+
132+
//Validate that the target file name is not a system file
133+
$this->validateNotSystemFile($realPath);
134+
//Validate that target folder (UPLOAD_DIR + ['name'] - ['basename']) is not a system folder
135+
$this->validateNotSystemFolder(preg_replace('/\/[^\/]+$/', '', $realPath));
136+
//Validate that given file doesn't exist or is own by current customer
137+
$existingMediaFileInfo = $this->findFileByRelativePath($realPath);
138+
if ($existingMediaFileInfo && $existingMediaFileInfo->getCustomerId() !== $customerId) {
139+
throw new ValidationException('Access denied');
140+
}
141+
142+
//Copy temp file to target path
143+
$uploadDir->getDriver()->copy(
144+
$uploadedFileData['tmp_name'],
145+
$realPath
146+
);
147+
148+
//Persist file info
149+
$mediaFile = new MediaFile($customerId, $realPath);
150+
return $this->persist($mediaFile);
151+
}
152+
}
153+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../v2.3/extension-dev-guide/security/uploads.md

0 commit comments

Comments
 (0)