Security
Headlines
HeadlinesLatestCVEs

Headline

CVE-2023-46253: RCE via Arbitrary File Write

Squidex is an open source headless CMS and content management hub. Affected versions are subject to an arbitrary file write vulnerability in the backup restore feature which allows an authenticated attacker to gain remote code execution (RCE). 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. 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. 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. The assetId is inserted into the filename without any sanitization and an attacker with squidex.admin.restore privileges to run arbitrary operating system commands on the underlying server (RCE).

CVE
#xss#vulnerability#js#rce#auth

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.

CVE: Latest News

CVE-2023-50976: Transactions API Authorization by oleiman · Pull Request #14969 · redpanda-data/redpanda
CVE-2023-6905
CVE-2023-6903
CVE-2023-6904
CVE-2023-3907