Skip to content

Commit c208460

Browse files
committed
Merge branch '4.x' into 5.x
2 parents d800cc1 + 8f34189 commit c208460

16 files changed

Lines changed: 371 additions & 68 deletions

File tree

docs/09-advanced/06-security.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,13 @@ The value of a `FileUpload` field is a string (or array of strings) pointing to
200200

201201
Filament allows this by default because legitimate features depend on it — for example, an action that sets a field to a pre-uploaded template file, or a "copy from another record" button. If your forms do not rely on such a flow, opt in to the built-in checks:
202202

203-
- For `FileUpload` fields, call [`preventFilePathTampering()`](../forms/file-upload#authorizing-existing-file-paths) to drop submitted paths that do not match the original value on the record.
204-
- For `RichEditor` fields, call [`preventFileAttachmentPathTampering()`](../forms/rich-editor#securing-file-attachment-ids) to strip `data-id` values that are not already present in the record's stored content.
203+
- For `FileUpload` fields, call [`preventFilePathTampering()`](../forms/file-upload#authorizing-existing-file-paths) to fail validation when a submitted path does not match the original value on the record.
204+
- For `RichEditor` fields, call [`preventFileAttachmentPathTampering()`](../forms/rich-editor#securing-file-attachment-ids) to fail validation when a submitted `data-id` is not already present in the record's stored content.
205205

206206
Both methods compare submitted values against the attribute on the record via `$record->getOriginal()`, and both accept an `allowFilePathUsing` callback for paths that are legitimately added outside the record (such as shared template files). Newly uploaded files and images always pass through unchanged.
207207

208208
<Aside variant="warning">
209-
These checks require a record on the form, so on create pages every submitted existing path is rejected unless the `allowFilePathUsing` callback approves it. New uploads are unaffected.
209+
These checks require a record on the form, so on create pages every submitted existing path fails validation unless the `allowFilePathUsing` callback approves it. New uploads are unaffected.
210210
</Aside>
211211

212212
If you want these checks to apply across your entire application rather than remembering to add them to each field, enable them globally from a service provider's `boot()` method using `configureUsing()`:

packages/actions/src/Exports/Jobs/CreateXlsxFile.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ class CreateXlsxFile implements ShouldQueue
2626

2727
public bool $deleteWhenMissingModels = true;
2828

29+
public ?int $tries = 3;
30+
31+
public ?int $maxExceptions = 0;
32+
33+
/** @var array<int> */
34+
public array $backoff = [30, 60, 300];
35+
2936
protected Exporter $exporter;
3037

3138
/**

packages/actions/src/Exports/Jobs/ExportCompletion.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class ExportCompletion implements ShouldQueue
2323

2424
public bool $deleteWhenMissingModels = true;
2525

26+
public ?int $tries = 1;
27+
28+
public ?int $maxExceptions = 0;
29+
2630
protected Exporter $exporter;
2731

2832
/**

packages/actions/src/Exports/Jobs/ExportCsv.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ public function middleware(): array
6363

6464
public function handle(): void
6565
{
66+
if ($this->batch()?->cancelled()) {
67+
return;
68+
}
69+
6670
/** @var Authenticatable $user */
6771
$user = $this->export->user;
6872

packages/actions/src/Exports/Jobs/PrepareCsvExport.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class PrepareCsvExport implements ShouldQueue
2929

3030
public bool $deleteWhenMissingModels = true;
3131

32+
public ?int $tries = 1;
33+
34+
public ?int $maxExceptions = 0;
35+
3236
protected Exporter $exporter;
3337

3438
/**
@@ -52,6 +56,10 @@ public function __construct(
5256

5357
public function handle(): void
5458
{
59+
if ($this->batch()?->cancelled()) {
60+
return;
61+
}
62+
5563
$csv = Writer::from(new SplTempFileObject);
5664
$csv->setOutputBOM(Bom::Utf8);
5765
$csv->setDelimiter($this->exporter::getCsvDelimiter());

packages/actions/src/Imports/Jobs/ImportCsv.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ public function middleware(): array
6464

6565
public function handle(): void
6666
{
67+
if ($this->batch()?->cancelled()) {
68+
return;
69+
}
70+
6771
/** @var Authenticatable $user */
6872
$user = $this->import->user;
6973

packages/forms/docs/09-file-upload.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,10 @@ FileUpload::make('avatar')
196196
->preventFilePathTampering()
197197
```
198198

199-
Filament compares every submitted string path against the value originally loaded from the record (via `$record->getOriginal()` for the attribute matching the field name). Paths that do not match are dropped before the record is saved and before any URL is generated for the field. Newly uploaded files always pass through, the field can still be cleared, and for `multiple()` fields each entry is checked individually.
199+
Filament compares every submitted string path against the value originally loaded from the record (via `$record->getOriginal()` for the attribute matching the field name). Paths that do not match cause the field to fail validation, so the record is never saved with a tampered value. Newly uploaded files always pass through, the field can still be cleared, and for `multiple()` fields each entry is checked individually.
200200

201201
<Aside variant="warning">
202-
`preventFilePathTampering()` needs a record on the form. Without one — for example, on a create page — every submitted string path is rejected unless the [`allowFilePathUsing`](#allowing-additional-file-paths-with-a-callback) callback approves it. New uploads are unaffected.
202+
`preventFilePathTampering()` needs a record on the form. Without one — for example, on a create page — every submitted string path fails validation unless the [`allowFilePathUsing`](#allowing-additional-file-paths-with-a-callback) callback approves it. New uploads are unaffected.
203203
</Aside>
204204

205205
To apply this check to every `FileUpload` in your application without repeating it on each field, call `configureUsing()` in a service provider's `boot()` method:
@@ -216,7 +216,7 @@ Individual fields can still opt out by calling `preventFilePathTampering(false)`
216216

217217
### Allowing additional file paths with a callback
218218

219-
If your application legitimately references a path that is not on the record — for example, a button that selects a pre-uploaded template file — pass the `allowFilePathUsing` argument to approve it:
219+
If your application legitimately references a path that is not on the record — for example, a button that selects a pre-uploaded template file — pass the `allowFilePathUsing` argument to approve it. Approved paths bypass the validation error:
220220

221221
```php
222222
use Filament\Forms\Components\FileUpload;
@@ -229,6 +229,18 @@ FileUpload::make('avatar')
229229

230230
<UtilityInjection set="formFields" version="5.x" extras="File;;string;;$file;;The submitted file path being authorized.">You can inject various utilities into the function passed to `allowFilePathUsing` as parameters.</UtilityInjection>
231231

232+
The validation error message can be customized via [`validationMessages()`](validation#customizing-validation-messages) using the `tampered` key:
233+
234+
```php
235+
use Filament\Forms\Components\FileUpload;
236+
237+
FileUpload::make('avatar')
238+
->preventFilePathTampering()
239+
->validationMessages([
240+
'tampered' => 'The selected attachment is not permitted.',
241+
])
242+
```
243+
232244
## Avatar mode
233245

234246
You can enable avatar mode for your file upload field using the `avatar()` method:

packages/forms/docs/10-rich-editor.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,12 @@ RichEditor::make('content')
412412
->preventFileAttachmentPathTampering()
413413
```
414414

415-
Filament parses the record's original content (via `$record->getOriginal()` for the attribute matching the field name) and allows only the `data-id` values already present. Any other existing `data-id` has its `id` and `src` attributes removed before the record is saved and before any URL is generated. Newly uploaded images always pass through.
415+
Filament parses the record's original content (via `$record->getOriginal()` for the attribute matching the field name) and allows only the `data-id` values already present. Any other existing `data-id` causes the field to fail validation, so the record is never saved with a tampered value. Newly uploaded images always pass through.
416416

417417
If you are using the [`spatie/laravel-medialibrary` plugin](https://filamentphp.com/plugins/filament-spatie-media-library#using-media-library-for-rich-editor-file-attachments) as the file attachment provider, this protection is already implicit — it looks up each `data-id` against the record's own media collection.
418418

419419
<Aside variant="warning">
420-
`preventFileAttachmentPathTampering()` needs a record on the form. Without one — for example, on a create page — every existing `data-id` is rejected unless the [`allowFilePathUsing`](#allowing-additional-data-id-values-with-a-callback) callback approves it. New uploads are unaffected.
420+
`preventFileAttachmentPathTampering()` needs a record on the form. Without one — for example, on a create page — every existing `data-id` fails validation unless the [`allowFilePathUsing`](#allowing-additional-data-id-values-with-a-callback) callback approves it. New uploads are unaffected.
421421
</Aside>
422422

423423
To apply this check to every `RichEditor` in your application without repeating it on each field, call `configureUsing()` in a service provider's `boot()` method:
@@ -434,7 +434,7 @@ Individual fields can still opt out by calling `preventFileAttachmentPathTamperi
434434

435435
#### Allowing additional `data-id` values with a callback
436436

437-
If your application legitimately references an identifier that is not on the record — for example, a "copy from another record" action — pass the `allowFilePathUsing` argument to approve it:
437+
If your application legitimately references an identifier that is not on the record — for example, a "copy from another record" action — pass the `allowFilePathUsing` argument to approve it. Approved identifiers bypass the validation error:
438438

439439
```php
440440
use Filament\Forms\Components\RichEditor;
@@ -447,6 +447,18 @@ RichEditor::make('content')
447447

448448
<UtilityInjection set="formFields" version="5.x" extras="File;;string;;$file;;The submitted `data-id` value being authorized.">You can inject various utilities into the function passed to `allowFilePathUsing` as parameters.</UtilityInjection>
449449

450+
The validation error message can be customized via [`validationMessages()`](validation#customizing-validation-messages) using the `tampered` key:
451+
452+
```php
453+
use Filament\Forms\Components\RichEditor;
454+
455+
RichEditor::make('content')
456+
->preventFileAttachmentPathTampering()
457+
->validationMessages([
458+
'tampered' => 'The content references an image that is not permitted.',
459+
])
460+
```
461+
450462
### Validating uploaded images
451463

452464
You may use the `fileAttachmentsAcceptedFileTypes()` method to control a list of accepted mime types for uploaded images. By default, `image/png`, `image/jpeg`, `image/gif`, and `image/webp` are accepted:

packages/forms/resources/lang/en/validation.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@
77
'only_one_must_be_selected' => 'Only one :attribute field must be selected.',
88
],
99

10+
'tampered_file_path' => 'The :attribute field contains a file path that is not permitted.',
11+
1012
];

packages/forms/src/Components/BaseFileUpload.php

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,30 @@ public function getValidationRules(): array
692692
...$arrayRules,
693693
];
694694

695+
if ($this->shouldPreventFilePathTampering()) {
696+
$rules[] = function (string $attribute, mixed $value, Closure $fail): void {
697+
$originalPaths = $this->getOriginalFilePaths();
698+
699+
foreach (Arr::wrap($value) as $file) {
700+
if ($file instanceof TemporaryUploadedFile) {
701+
continue;
702+
}
703+
704+
if (! is_string($file)) {
705+
continue;
706+
}
707+
708+
if ($this->isFilePathAuthorized($file, $originalPaths)) {
709+
continue;
710+
}
711+
712+
$fail(__($this->getValidationMessages()['tampered'] ?? 'filament-forms::validation.tampered_file_path', ['attribute' => $this->getValidationAttribute()]));
713+
714+
return;
715+
}
716+
};
717+
}
718+
695719
$rules[] = function (string $attribute, array $value, Closure $fail) use ($fileRules): void {
696720
$files = array_filter($value, fn (TemporaryUploadedFile | string $file): bool => $file instanceof TemporaryUploadedFile);
697721

@@ -915,10 +939,6 @@ public function saveUploadedFiles(): void
915939
return;
916940
}
917941

918-
if ($this->shouldPreventFilePathTampering()) {
919-
$this->dropTamperedFilePaths();
920-
}
921-
922942
$rawState = array_filter(array_map(function (TemporaryUploadedFile | string $file) {
923943
// String values represent paths to files that already exist on the disk, and
924944
// are passed through unchanged. Like any Livewire form field value, this
@@ -963,27 +983,6 @@ public function saveUploadedFiles(): void
963983
$this->callAfterStateUpdated();
964984
}
965985

966-
protected function dropTamperedFilePaths(): void
967-
{
968-
$originalPaths = $this->getOriginalFilePaths();
969-
970-
$filtered = [];
971-
972-
foreach (Arr::wrap($this->getRawState()) as $key => $file) {
973-
if ($file instanceof TemporaryUploadedFile) {
974-
$filtered[$key] = $file;
975-
976-
continue;
977-
}
978-
979-
if (is_string($file) && $this->isFilePathAuthorized($file, $originalPaths)) {
980-
$filtered[$key] = $file;
981-
}
982-
}
983-
984-
$this->rawState($filtered);
985-
}
986-
987986
/**
988987
* @param array<string> | null $originalPaths
989988
*/

0 commit comments

Comments
 (0)