A file upload field is a door that lets visitors put arbitrary bytes onto your server — even a well-permissioned form becomes a liability if it accepts anything named right. #upload_validators is how you narrow that door: a list of rules Drupal checks before the file is ever saved, rejecting anything that doesn't fit.
What you'll learn in this lesson
- How to restrict an upload field to specific file extensions and a maximum size
- Why
setPermanent()matters, and what happens if you forget it - What genuinely changed for file validation in recent core versions
A validated managed_file field
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\file\Entity\File;
class ImageUploadForm extends FormBase {
public function getFormId() {
return 'image_upload_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form['image'] = [
'#type' => 'managed_file',
'#title' => $this->t('Upload an Image'),
'#upload_location' => 'public://images/',
'#upload_validators' => [
'file_validate_extensions' => ['png jpg jpeg'],
'file_validate_size' => [2 * 1024 * 1024], // 2MB in bytes.
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Upload'),
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$fids = $form_state->getValue('image');
if ($fids) {
$file = File::load($fids[0]);
$file->setPermanent();
$file->save();
$this->messenger()->addMessage($this->t('Image uploaded successfully!'));
}
else {
$this->messenger()->addMessage($this->t('Image upload failed. Please try again.'), 'error');
}
}
}
drupal_set_message() calls — removed in Drupal 8 — are now $this->messenger()->addMessage(). The array key is also renamed from a generic $file to $fids, since #type => 'managed_file' actually stores an array of file IDs, not a file object — that's exactly why the next line has to File::load() it.Why the two validators matter separately
file_validate_extensions checks the filename's extension against an allow-list — it does not inspect the file's actual contents, so it's a first line of defense, not a guarantee the file truly is an image. file_validate_size rejects anything over the byte limit before it's fully processed, protecting against both storage abuse and a poorly-behaved upload tying up server resources.
Why setPermanent() is not optional
A freshly-uploaded managed file starts life as temporary — Drupal's file garbage collection will delete any temporary file that isn't attached to something within roughly 6 hours (governed by the file_temporary_maximum_age setting). Skip setPermanent() and your users' uploads will quietly vanish shortly after submission, which reads as a mysterious bug rather than the actually-documented cleanup behavior it is.
FileValidator plugin system (\Drupal\file\Validation\FileValidatorInterface) intended to eventually replace the classic string-keyed #upload_validators array shown above. As of this writing, the classic syntax above still works across Drupal 9, 10, and 11 and remains what you'll see throughout most existing modules — but if you're specifically targeting the newest core versions, be aware the plugin-based system exists as the forward-looking replacement.Quick check: a user uploads a 50KB file named
photo.jpg.php. Doesfile_validate_extensions => ['png jpg jpeg']block it? (Answer: yes — the validator checks the file's actual final extension, which here is.php, not just whether "jpg" appears anywhere in the filename. This is exactly the kind of double-extension trick the validator exists to catch.)
Key takeaways
#upload_validatorsruns before a file is accepted — restrict extensions and size here rather than checking after the fact.managed_filestores an array of file IDs in the form value, not a file object — alwaysFile::load()before treating it as one.- Always call
setPermanent()andsave()on an uploaded file you intend to keep — temporary files are automatically garbage-collected. - Extension validation checks the filename pattern, not file contents — it's a first defense, not a full content-type guarantee.
drupal_set_message()is dead Drupal 7 code — use the Messenger service instead.
Coming up next
You've now covered CSRF tokens, input sanitization, permission-based access, and file upload validation — the core security surface of any Drupal form. Next, we close out this section by looking at limiting form access more broadly: combining these techniques to control exactly who can reach a form and under what conditions.