CVE-2025-25599: A Cautionary Tale of Insecure Temporary Files
During a security assessment of Bolt, an open-source content management system, it was discovered that temporary files are used insecurely when uploading an avatar from a URL, leading to arbitrary file disclosure (CVE-2025-25599):

The source code for the backend function which handles such requests is listed below. Note the four lines highlighted; we can see that the backend performs these steps in the following order:
- A temporary folder is created
- The file pointed to by the URL is downloaded into the temporary folder
- The function
handleUpload
is executed - The temporary file is deleted
/**
* @Route("/upload-url", name="bolt_async_upload_url", methods={"POST"})
*/
public function handleURLUpload(Request $request): Response
{
try {
$this->validateCsrf('upload');
} catch (InvalidCsrfTokenException $e) {
return new JsonResponse([
'error' => [
'message' => 'Invalid CSRF token',
],
], Response::HTTP_FORBIDDEN);
}
$url = $request->get('url', '');
$filename = basename($url);
$locationName = $request->get('location', '');
$path = $request->get('path') . $filename;
$folderpath = $this->config->getPath($locationName, true, 'tmp/');
$target = $this->config->getPath($locationName, true, 'tmp/' . $path);
try {
// Make sure temporary folder exists
$this->filesystem->mkdir($folderpath);
// Create temporary file
$this->filesystem->copy($url, $target);
} catch (Throwable $e) {
return new JsonResponse([
'error' => [
'message' => $e->getMessage(),
],
], Response::HTTP_BAD_REQUEST);
}
$file = new UploadedFile($target, $filename);
$bag = new FileBag();
$bag->add([$file]);
$request->files = $bag;
$response = $this->handleUpload($request);
// The file is automatically deleted. It may be that we don't need this.
$this->filesystem->remove($target);
return $response;
}
Looking at the source code for handleUpload
, we see that before setting a new avatar, various validation checks are carried out against the file size, extension, and contents. If all checks pass, only then is the user’s avatar updated.
...SNIP...
$acceptedFileTypes = array_merge($this->config->getMediaTypes()->toArray(), $this->config->getFileTypes()->toArray());
$maxSize = $this->config->getMaxUpload();
$uploadHandler->addRule(
'extension',
['allowed' => $acceptedFileTypes],
'The file for field \'{label}\' was <u>not</u> uploaded. It should be a valid file type. Allowed are <code>' . implode('</code>, <code>', $acceptedFileTypes) . '.',
'Upload file'
);
$uploadHandler->addRule(
'size',
['size' => $maxSize],
'The file for field \'{label}\' was <u>not</u> uploaded. The upload can have a maximum filesize of <b>' . $this->textExtension->formatBytes($maxSize) . '</b>.',
'Upload file'
);
$uploadHandler->addRule(
'callback',
['callback' => [$this, 'checkJavascriptInSVG']],
'It is not allowed to upload SVG\'s with embedded Javascript.',
'Upload file'
);
$uploadHandler->setSanitizerCallback(function ($name) {
return $this->sanitiseFilename($name);
});
...SNIP...
So, what is the issue? It turns out that the temporary folder is [BOLT_PATH]/public/files/tmp
, which, as the name suggests, is publicly accessible (e.g. http://example.com/files/tmp/). This means that for the few milliseconds they exist, temporary files may be downloaded. Furthermore, avatar URLs are not properly validated, and it is possible to “upload” local files using the file://
protocol, e.g. file:///etc/passwd.

Either by reviewing the source code, or by monitoring the temporary folder, we can determine the naming convention for temporary files. In the case of /etc/passwd
, a temporary file called avatarspasswd
is created.
root@host:/opt/bolt/public/files/tmp# while [ 1==1 ]; do ls; done
avatarspasswd
avatarspasswd
avatarspasswd
avatarspasswd
avatarspasswd
^C
This means that if an attacker were able to time the request correctly, they could download the file from http://example.com/files/tmp/avatarspasswd. To demonstrate this exploit manually, we can first set up the following BurpSuite Intruder attack to spam download attempts:

Next, intercept a request to upload an avatar from file:///etc/passwd
and send it to Repeater:

With both ready, we can go ahead and start the Intruder attack, then spam the upload request. The goal is to send a request to download the temporary file before it gets deleted. If the attack was successfull, there will be a few responses with a 200 OK
status code, containing the contents of /etc/passwd
.

Conclusion
Patches addressing this issue on the 5.1.x and 5.2.x branches were released on 2025-03-11. Our full advisory is available here.
A Docker container for testing, as well as a proof of concept script for reproducing this exploit is available on our GitHub.