Skip to content

Commit 3c6dbf5

Browse files
authored
Merge pull request dilab#39 from code-lts/security-fix
Fix remote arbitrary file upload in any directory of the file system
2 parents 62c3fff + d3552ef commit 3c6dbf5

File tree

3 files changed

+61
-14
lines changed

3 files changed

+61
-14
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"require": {
1111
"php": ">=8.1.0",
1212
"cakephp/filesystem": "^3.0",
13-
"monolog/monolog": "^2.0"
13+
"monolog/monolog": "^2.0",
14+
"ondrej-vrto/php-filename-sanitize": "^1.4"
1415
},
1516
"require-dev": {
1617
"phpunit/phpunit": "~10.0"

src/Resumable.php

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Dilab\Network\Response;
99
use Monolog\Logger;
1010
use Monolog\Handler\StreamHandler;
11+
use OndrejVrto\FilenameSanitize\FilenameSanitize;
1112

1213
class Resumable
1314
{
@@ -159,18 +160,14 @@ public function getExtension()
159160
}
160161

161162
/**
162-
* Makes sure the orginal extension never gets overriden by user defined filename.
163+
* Creates a safe name
163164
*
164-
* @param string User defined filename
165-
* @param string Original filename
166-
* @return string Filename that always has an extension from the original file
165+
* @param string $name Original name
166+
* @return string A safer name
167167
*/
168-
private function createSafeFilename($filename, $originalFilename)
168+
private function createSafeName(string $name): string
169169
{
170-
$filename = $this->removeExtension($filename);
171-
$extension = $this->findExtension($originalFilename);
172-
173-
return sprintf('%s.%s', $filename, $extension);
170+
return FilenameSanitize::of($name)->get();
174171
}
175172

176173
public function handleTestChunk()
@@ -227,9 +224,9 @@ private function createFileAndDeleteTmp($identifier, $filename)
227224

228225
// if the user has set a custom filename
229226
if (null !== $this->filename) {
230-
$finalFilename = $this->createSafeFilename($this->filename, $filename);
227+
$finalFilename = $this->createSafeName($this->filename);
231228
} else {
232-
$finalFilename = $filename;
229+
$finalFilename = $this->createSafeName($filename);
233230
}
234231

235232
// replace filename reference by the final file
@@ -288,7 +285,7 @@ public function tmpChunkDir($identifier)
288285
if (!empty($this->instanceId)){
289286
$tmpChunkDir .= $this->instanceId . DIRECTORY_SEPARATOR;
290287
}
291-
$tmpChunkDir .= $identifier;
288+
$tmpChunkDir .= $this->createSafeName($identifier);
292289
$this->ensureDirExists($tmpChunkDir);
293290
return $tmpChunkDir;
294291
}
@@ -318,7 +315,7 @@ private function ensureDirExists($path)
318315

319316
public function tmpChunkFilename($filename, $chunkNumber)
320317
{
321-
return $filename . '.' . str_pad($chunkNumber, 4, 0, STR_PAD_LEFT);
318+
return $this->createSafeName($filename) . '.' . str_pad($chunkNumber, 4, 0, STR_PAD_LEFT);
322319
}
323320

324321
public function getExclusiveFileHandle($name)

test/src/ResumableTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,55 @@ public function testTmpChunkFile()
235235
$this->assertEquals($expected, $this->resumbable->tmpChunkFilename($filename,$chunkNumber));
236236
}
237237

238+
public static function fileNameProvider(): array
239+
{
240+
return [
241+
['example-file.png', 'example-file.png'],
242+
['../unsafe-one-level.txt', 'unsafe-one-level.txt'],
243+
];
244+
}
245+
246+
/**
247+
* @dataProvider fileNameProvider
248+
*/
249+
public function testResumableSanitizeFileName(string $filename, string $filenameSanitized): void
250+
{
251+
$resumableParams = array(
252+
'resumableChunkNumber'=> 1,
253+
'resumableTotalChunks'=> 1,
254+
'resumableChunkSize'=> 200,
255+
'resumableIdentifier'=> 'identifier',
256+
'resumableFilename'=> $filename,
257+
'resumableRelativePath'=> 'upload',
258+
);
259+
260+
261+
$this->request->method('is')
262+
->willReturn(true);
263+
264+
$this->request->method('data')
265+
->willReturn($resumableParams);
266+
267+
$this->request->method('file')
268+
->willReturn(array(
269+
'name'=> 'mock.png',
270+
'tmp_name'=> 'test/files/mock.png.0003',
271+
'error'=> 0,
272+
'size'=> 27000,
273+
));
274+
275+
$this->resumbable = new Resumable($this->request, $this->response);
276+
$this->resumbable->tempFolder = 'test/tmp';
277+
$this->resumbable->uploadFolder = 'test/uploads';
278+
$this->resumbable->deleteTmpFolder = false;
279+
$this->resumbable->handleChunk();
280+
281+
$this->assertFileExists('test/uploads/' . $filenameSanitized);
282+
$this->assertFileExists('test/tmp/identifier/' . $filenameSanitized . '.0001');
283+
$this->assertTrue(unlink('test/tmp/identifier/' . $filenameSanitized . '.0001'));
284+
$this->assertTrue(unlink('test/uploads/' . $filenameSanitized));
285+
}
286+
238287
public function testCreateFileFromChunks()
239288
{
240289
$files = array(

0 commit comments

Comments
 (0)