Summary
An arbitrary file write vulnerability in the backup restore feature allows an authenticated attacker to gain remote code execution (RCE).
Details
Squidex allows users with the squidex.admin.restore
permission to create and restore backups. Part of these backups are the assets uploaded to an App. For each asset, the backup zip archive contains a .asset
file with the actual content of the asset as well as a related AssetCreatedEventV2
event, which is stored in a JSON file:
$ zipinfo backup.zip
...
-rw-r--r-- 3.0 unx 43 tx stor 23-Oct-09 12:04 attachments/176/46c05041-9588-4179-b5eb-ddfcd9463e1e_0.asset
-rw-r--r-- 3.0 unx 1184 tx defN 23-Oct-09 12:04 events/0/4.json
...
Amongst other things, the JSON file contains the event type (AssetCreatedEventV2
), the ID of the asset (46c05041-9588-4179-b5eb-ddfcd9463e1e
), its filename (test.txt
), and its file version (0
):
$ cat events/0/4.json
{"n":{"t":"AssetCreatedEventV2","s":"asset-33e55647-7db1-4a20-b27b-0f690753a5d5--46c05041-9588-4179-b5eb-ddfcd9463e1e","p":"{\u0022parentId\u0022:\u002200000000-0000-0000-0000-000000000000\u0022,\u0022fileName\u0022:\u0022test.txt\u0022,\u0022fileHash\u0022:\u0022tguDdazklWWU13e5N/3kflTT9vxjvjy4gyxByFts5V8=\u0022,\u0022mimeType\u0022:\u0022text/plain\u0022,\u0022slug\u0022:\u0022test.txt\u0022,\u0022fileVersion\u0022:0,\u0022fileSize\u0022:43,\u0022type\u0022:\u0022Unknown\u0022,\u0022metadata\u0022:{},...}
When a backup with this event is restored, the BackupAssets.ReadAssetAsync
method is responsible for re-creating the asset. For this purpose, it determines the name of the .asset
file in the zip archive, reads its content, and stores the content in the filestore (by default FolderAssetStore):
private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader,
CancellationToken ct)
{
try
{
// (1) get name of .asset file in zip archive
var fileName = GetName(assetId, fileVersion);
// (2) read content of .asset file
await using (var stream = await(fileName, ct))
{
// (3) store content in filestore
await assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream, true, ct);
}
}
catch (FileNotFoundException)
{
return;
}
}
The GetName
method constructs the name of .asset
file by using the assetId
and fileVersion
:
private static string GetName(DomainId assetId, long fileVersion)
{
return $"{assetId}_{fileVersion}.asset";
}
Please notice that this filename is only used to retrieve the .asset
file from the zip archive. When the asset is stored in the filestore via the UploadAsync
method, the assetId
and fileVersion
are passed as arguments. These are further passed to the method GetFileName
, which determines the filename where the asset should be stored:
public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true,
CancellationToken ct = default)
{
var fileName = GetFileName(appId, id, fileVersion, suffix);
return assetStore.UploadAsync(fileName, stream, overwrite, ct);
}
The GetFileName
is slightly different from the GetName
method. Again, the assetId
(parameter id
) is used, but the fileVersion
is only appended, if it is equal or greater than zero. Also, there is no suffix by default (no .asset
extension):
private string GetFileName(DomainId appId, DomainId id, long fileVersion = -1, string? suffix = null)
{
var sb = new StringBuilder(20);
// ...
sb.Append(id);
if (fileVersion >= 0)
{
sb.Append('_');
sb.Append(fileVersion);
}
// ...
return sb.ToString();
}
In both cases, for the retrieval (GetName
) and the storage (GetFileName
) of an asset file, the assetId
is inserted into the filename without any sanitization.
Impact
The vulnerability allows an attacker with squidex.admin.restore
privileges to run arbitrary operating system commands on the underlying server (RCE).
An unauthenticated attacker can combine this vulnerability with an XSS vulnerability to trigger the vulnerability in the context of a user with the required privileges and restore a malicious backup to take over the Squidex instance.
Summary
An arbitrary file write vulnerability in the backup restore feature allows an authenticated attacker to gain remote code execution (RCE).
Details
Squidex allows users with the
squidex.admin.restore
permission to create and restore backups. Part of these backups are the assets uploaded to an App. For each asset, the backup zip archive contains a.asset
file with the actual content of the asset as well as a relatedAssetCreatedEventV2
event, which is stored in a JSON file:Amongst other things, the JSON file contains the event type (
AssetCreatedEventV2
), the ID of the asset (46c05041-9588-4179-b5eb-ddfcd9463e1e
), its filename (test.txt
), and its file version (0
):When a backup with this event is restored, the
BackupAssets.ReadAssetAsync
method is responsible for re-creating the asset. For this purpose, it determines the name of the.asset
file in the zip archive, reads its content, and stores the content in the filestore (by default FolderAssetStore):The
GetName
method constructs the name of.asset
file by using theassetId
andfileVersion
:Please notice that this filename is only used to retrieve the
.asset
file from the zip archive. When the asset is stored in the filestore via theUploadAsync
method, theassetId
andfileVersion
are passed as arguments. These are further passed to the methodGetFileName
, which determines the filename where the asset should be stored:The
GetFileName
is slightly different from theGetName
method. Again, theassetId
(parameterid
) is used, but thefileVersion
is only appended, if it is equal or greater than zero. Also, there is no suffix by default (no.asset
extension):In both cases, for the retrieval (
GetName
) and the storage (GetFileName
) of an asset file, theassetId
is inserted into the filename without any sanitization.Impact
The vulnerability allows an attacker with
squidex.admin.restore
privileges to run arbitrary operating system commands on the underlying server (RCE).An unauthenticated attacker can combine this vulnerability with an XSS vulnerability to trigger the vulnerability in the context of a user with the required privileges and restore a malicious backup to take over the Squidex instance.