Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 968fe87

Browse files
committed
bug #7577 Prevent ImageField from overwriting existing files (lacatoire)
This PR was squashed before being merged into the 5.x branch. Discussion ---------- Prevent ImageField from overwriting existing files The default `upload_validate` callable in `FileUploadType` checks `file_exists()` to avoid collisions, but it only received the file basename, so the check was always false and existing files were silently overwritten. `StringToFileTransformer` now passes the full `upload_dir + filename` path to `upload_validate` and strips the prefix from the returned value, restoring the intended collision detection for both local and Flysystem storage. Fixes #7525 Commits ------- 7ede15d Prevent ImageField from overwriting existing files
2 parents 0448019 + 7ede15d commit 968fe87

3 files changed

Lines changed: 79 additions & 2 deletions

File tree

src/Form/DataTransformer/StringToFileTransformer.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,14 @@ private function doReverseTransform(mixed $value): ?string
141141
}
142142

143143
$filename = ($this->uploadFilename)($value);
144-
145-
return ($this->uploadValidate)($filename);
144+
// Pass the full path (upload dir + filename) to the validator so
145+
// it can detect existing files and avoid overwriting them. Strip
146+
// the upload dir afterwards so only the relative name is stored.
147+
$validatedPath = ($this->uploadValidate)($this->uploadDir.$filename);
148+
149+
return str_starts_with($validatedPath, $this->uploadDir)
150+
? substr($validatedPath, \strlen($this->uploadDir))
151+
: $validatedPath;
146152
}
147153

148154
if ($value instanceof FlysystemFile) {

src/Form/Type/FileUploadType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ public function configureOptions(OptionsResolver $resolver): void
175175
// no file were stored. Overrides must preserve this contract.
176176
$uploadFilename = static fn (UploadedFile $file): string => basename(str_replace('\\', '/', $file->getClientOriginalName()));
177177

178+
// the upload_validate callable receives the full path (upload_dir + filename)
179+
// so it can call file_exists() to detect collisions. Its return value may be
180+
// either a full path inside upload_dir (the leading upload_dir will be stripped
181+
// by StringToFileTransformer) or a plain relative filename. In both cases the
182+
// value that is finally stored must satisfy the same safe-path contract as
183+
// upload_filename (see comment above).
178184
$uploadValidate = static function (string $filename): string {
179185
if (!file_exists($filename)) {
180186
return $filename;

tests/Unit/Form/DataTransformer/StringToFileTransformerTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPUnit\Framework\TestCase;
77
use Symfony\Component\Filesystem\Filesystem;
88
use Symfony\Component\HttpFoundation\File\File;
9+
use Symfony\Component\HttpFoundation\File\UploadedFile;
910

1011
class StringToFileTransformerTest extends TestCase
1112
{
@@ -74,6 +75,70 @@ public static function unsafeStoredPathProvider(): iterable
7475
yield 'leading backslash' => ['\\etc\\passwd'];
7576
}
7677

78+
public function testReverseTransformPassesFullPathToUploadValidate(): void
79+
{
80+
$tmpFile = tempnam(sys_get_temp_dir(), 'ea_stf_');
81+
file_put_contents($tmpFile, 'new content');
82+
$uploaded = new UploadedFile($tmpFile, 'normal.txt', 'text/plain', null, true);
83+
84+
$receivedByValidate = null;
85+
$transformer = new StringToFileTransformer(
86+
$this->uploadDir,
87+
static fn (UploadedFile $file): string => $file->getClientOriginalName(),
88+
static function (string $filename) use (&$receivedByValidate): string {
89+
$receivedByValidate = $filename;
90+
91+
return $filename;
92+
},
93+
false,
94+
);
95+
96+
$transformer->reverseTransform($uploaded);
97+
98+
self::assertSame($this->uploadDir.'normal.txt', $receivedByValidate);
99+
100+
@unlink($tmpFile);
101+
}
102+
103+
public function testReverseTransformStripsUploadDirFromValidateResult(): void
104+
{
105+
$tmpFile = tempnam(sys_get_temp_dir(), 'ea_stf_');
106+
file_put_contents($tmpFile, 'new content');
107+
$uploaded = new UploadedFile($tmpFile, 'normal.txt', 'text/plain', null, true);
108+
109+
// Default-style validator: if the file exists on disk, append a numeric
110+
// suffix to avoid overwriting the existing file.
111+
$uploadValidate = static function (string $filename): string {
112+
if (!file_exists($filename)) {
113+
return $filename;
114+
}
115+
116+
$index = 1;
117+
$pathInfo = pathinfo($filename);
118+
while (file_exists($filename = sprintf('%s/%s_%d.%s', $pathInfo['dirname'], $pathInfo['filename'], $index, $pathInfo['extension']))) {
119+
++$index;
120+
}
121+
122+
return $filename;
123+
};
124+
125+
$transformer = new StringToFileTransformer(
126+
$this->uploadDir,
127+
static fn (UploadedFile $file): string => $file->getClientOriginalName(),
128+
$uploadValidate,
129+
false,
130+
);
131+
132+
// "normal.txt" already exists in the upload dir (see setUp), so the
133+
// validator must rename the incoming file to avoid an overwrite, and
134+
// the returned value must be the relative filename (no upload dir).
135+
$result = $transformer->reverseTransform($uploaded);
136+
137+
self::assertSame('normal_1.txt', $result);
138+
139+
@unlink($tmpFile);
140+
}
141+
77142
private function createTransformer(): StringToFileTransformer
78143
{
79144
return new StringToFileTransformer(

0 commit comments

Comments
 (0)