Back Tantosec From Arbitrary File Read to Shell: Yet Another RCE in Telerik
Tanto Security chained two vulnerabilities in Telerik UI for ASP.NET AJAX into unauthenticated remote code execution. The first is in RadImageEditor, allowing an adversary to read arbitrary files from the web application directory (CVE-2026-18672). Progress Software reports ↗ (opens in a new tab) that the issues affect versions 2011.2.712 through 2026.2.708 inclusive.
Turning the read into code execution is configuration-dependent. It needs one of two web.config settings to be present, a static machineKey or a Telerik.Web.UI.DialogParametersEncryptionKey , neither of which are given by a default install. The second vulnerability (CVE-2026-19219) is an unrestricted file upload through RadEditor’s file browser, reachable with one of those keys.
Users of Telerik UI for ASP.NET AJAX should upgrade to version 2026.3.812 (2026 Q3) or later. As a defence-in-depth measure, they should also consider setting Telerik.Web.UI.FileBrowserAllowedPaths in web.config to restrict which folders the file browser can reach.
In the first post in this series, From Padding Oracle to Shell: RCE in Telerik UI for ASP.NET AJAX , we took an AES-CBC padding oracle in RadAsyncUpload through to unauthenticated remote code execution. Progress Software fixed that chain in version 2026.2.708. This post is what we found while checking if that fix resolved the reported issues.
When Progress Software was preparing version 2026.2.708, they sent us a preview build to validate. Re-running the old exploit and seeing it fail does not confirm a patch, so we pulled the new Telerik.Web.UI.dll apart to understand the fix. While the padding oracle chain had been resolved, we took the opportunity to explore how other controls handled the client state returned to them on postback. In doing so we uncovered an arbitrary file read primitive in RadImageEditor’s caching mechanism.
A file read on its own is an information leak. However, this one can read web.config , and with it the keys and other secrets a Telerik site operator may keep there. There are two keys within web.config that are not present by default, but which Progress Software recommends be manually configured ↗ (opens in a new tab) . These keys, if present in web.config , allow an adversary to turn the arbitrary file read into remote code execution.
RadImageEditor, RadEditor, and the dialog handler
The first post covered what Telerik UI for ASP.NET AJAX is and how a request reaches the code in Telerik.Web.UI.dll . The chain in this post runs through three parts of that library.
RadImageEditor is a server-side image editing control. It crops, resizes and rotates an image on the server, and it keeps its state in a hidden postback form field that bounces between the browser and the server. For a RadImageEditor, that field is named _ClientState . It is neither encrypted nor signed, so a client can edit it before sending it back.
RadEditor is Telerik’s rich-text editor, the HTML editing box you typically find in a content management system. It ships with various dialogs, such as an image editor, an image manager, and a document manager. Upon rendering, RadEditor’s client-side bootstrap, a component called RadDialogOpener, emits one encrypted parameter blob per dialog, containing configuration used by those controls. Telerik calls it SerializedParameters, but we will call it dp for short. On the wire it travels in a form field called dialogParametersHolder . Unlike the image editor client state, this blob is encrypted and authenticated by the server, so it cannot be changed without the encryption key.
Both controls talk to handlers that are registered once for the whole application, not per page.
Telerik.Web.UI.WebResource.axd is the router we discussed in the post. It reads a type value and dispatches to the target handler. type=iec reaches the cache handler ( ImageEditorCacheHandler ) that serves edited images back to the browser, and type=rau reaches the async upload handler.
Telerik.Web.UI.DialogHandler.aspx serves RadEditor’s dialogs.
Because these handlers are global to the application, the attack surface is not one special image editor page. Any page that renders a RadEditor pulls in the dialog machinery and the handlers behind it.
Info: A quick deconfliction
Three components in this chain have ImageEditor in their name, and they are not the same thing:
RadImageEditor is the control, the server-side image editor a developer drops on a page.
The ImageEditor dialog is the pop-up that hosts a RadImageEditor inside a RadEditor. Its registered DialogName is ImageEditor .
ImageEditorCacheHandler is the handler that serves edited images back over WebResource.axd?type=iec . We call it “the cache handler” from here on.
Once RadImageEditor is rendered in the browser, its JavaScript builds _ClientState with saveClientState , serialising the editor’s properties into the JSON that the field carries:
The two image properties look redundant, but they move in opposite directions. The server renders the control with a configured image and pushes that path down to the client as serverImageUrl . The client echoes it back untouched on every postback. imageUrl goes the other way. The client adds it only when the user loads a different image, and on postback the server reads imageUrl , not serverImageUrl , and assigns it to the control’s ImageUrl property. So imageUrl is how the client tells the server which image to load.
The field also carries the editor’s other state, currentCommand , imageKey , height , width , enableResize , toolBarPosition and isf , plus clientOps when there is undo history.
On postback that field comes back to the server. RadImageEditor’s base class, RadWebControl , loads it:
text is the raw _ClientState field, straight off the request. A plain JavaScriptSerializer turns it into a Dictionary , and that dictionary goes to LoadClientState . There is no decrypt step and no message authentication code. Nothing checks that the field is the one that the server emitted, and so the client can change every key and value in the dictionary.
Poisoning the download cache
Being able to fully control a dictionary is only useful if one of its values ends up somewhere dangerous. Here, we are in control of imageUrl . On postback it becomes a server-side file path, and the control stores that path in a download cache to serve the file back on a later request. So we follow imageUrl from the client state into that cache.
RadImageEditor overrides LoadClientState , the method that receives the dictionary from the base class. Here is the start of it:
Line 17 copies our imageUrl into the control’s ImageUrl property, so the server-side ImageUrl becomes whatever path we put in the client state.
Then the caching. As the page renders, during the PreRender step, the control stores whatever ImageUrl points at under a new cache entry:
DownloadKey is a fresh GUID, and the cache entry maps it to the imageUrl we posted. The cache holds the path as a plain string. In the same render, the control serialises the GUID, encrypts it, and returns it to the client as a property called _downloadKey :
This functionality comes with a condition. Earlier in the same PreRender step, before it caches the path, the control may try to load the image that the path points to. If that load fails, the request errors out before anything is cached, and no _downloadKey comes back.
The load goes through a content provider, Telerik’s pluggable file-access layer, which also backs RadEditor’s file browser dialogs. The default one, FileSystemContentProvider , reads from disk, and a developer can plug in their own to load images from somewhere else, such as a database.
RadImageEditor only loads its image this way when the ImageManager.EnableContentProvider switch is on. The switch is off by default. Telerik’s demo application turns it on for its image editor demo, and as we will see later, the RadEditor dialog always does.
We run a copy of that demo application in our lab and use it as our target throughout this post.
With the switch on, the control fetches the image through the provider:
GetImageFromContentProvider passes the path to the provider’s GetFile , and throws if nothing comes back:
As a proof of concept, we will use web.config within the application root as the target file to read. We will why this file is particularly useful to an adversary later on.
In the Telerik demo application, the RadImageEditor demo lives in /ImageEditor/Examples/Overview/ . If we try to cache the bare string web.config , the control first tries to read the file, because the demo switches ImageManager.EnableContentProvider on. It resolves the bare name from the page’s folder, so it looks for /ImageEditor/Examples/Overview/web.config , which does not exist.
The fetch throws, and the page answers with a 500. No cache entry is created for the file, so no _downloadKey is returned. Our lab’s event log records the reason, The 'web.config' path does not point to a valid image .
To reach the root web.config , we climb out of the page’s folder with ../../../web.config , which resolves to web.config within the application root. The fetch finds the file, and the cache stores the raw string, ../../../web.config , not the resolved path.
Without a content provider enabled, the file existence check doesn’t happen, and the path goes into the cache unchecked.
The following is an example client state payload. We post it back in the _ClientState field to poison the cache. The imageUrl targets web.config , climbing out of the page’s folder as described above.
Info: Why these fields?
LoadClientState reads several of these keys by direct index, so the JSON has to carry every field that the server expects to read. We can use null for the fields we do not care . The exception is clientOps , which we entirely leave out on purpose. If the JSON includes it, even as null , it is taken as an operation undo stack. This will cause the server to try to parse the file pointed to by imageUrl as image data, which for something like web.config will trigger a server-side error. We don’t need an undo stack, and so we do not include the field.
The flow is two requests. First an unauthenticated GET of a page that renders the control:
The response carries the hidden form fields we need. We keep the __VIEWSTATE to replay, and the client state field’s name, which is where our payload goes:
Then we do a POST to the same page with the poisoned client state. We replay the __VIEWSTATE from the GET unchanged, so ViewState validation passes. The ViewState MAC covers __VIEWSTATE , not the other hidden fields, so the server accepts the crafted ctl00_ContentPlaceholder1_RadImageEditor1_ClientState without checking it:
The response comes back with a fresh _downloadKey . This is the encrypted GUID, which is the cache key for the entry we just poisoned:
Reading the file back through the cache handler
The poisoned _downloadKey from the last section is an encrypted token. To turn it into file contents, we hand it to the cache handler, Telerik.Web.UI.WebResource.axd?type=iec . The handler decrypts the token back to the GUID and looks it up in the cache, which returns the string we planted. It then maps that string to a path on disk, checks the path, and returns the file’s bytes.
Well. The handler is supposed to return the file’s bytes.
The error page hides the details, but our lab’s event log records the reason, Cannot use a leading .. to exit above the top directory.
What’s going on here? We used the right number of ../ traversals to keep the cache entry creation file-existence check happy. Why is it that once we get to downloading it via the cache handler, we’re apparently blowing past the “top directory”?
To understand what’s going on, let’s walk through the code on the download path.
The handler’s entry point, ProcessRequest , dispatches on a dkey query parameter:
An absent or empty dkey , or 1 , serves an image from the provider, and 2 serves a canvas image. Only a dkey that is present and is not 1 or 2 falls through to GetImageFromFileSystem , the branch that reads from the file system, and so this is why in our failing request we sent dkey=3 .
The mapping and the check live in ResolveFilePathFromKey , which GetImageFromFileSystem calls to turn the cached string into a path:
context.Cache.Get(downloadKey) returns the string we planted, "../../../web.config" , and MapPath turns it into a physical path. This is a different MapPath from the one in the first stage. GetFile used Server.MapPath , which resolves a relative path from the folder of the current request. Here the handler passes context.Request.ApplicationPath as the base folder, so a relative path resolves from the application root, wherever the request comes from.
So the two stages disagree where a relative path starts. The fetch, which runs when the path is cached, resolves it from the RadImageEditor page’s folder, so it needs the traversal to climb up to the web.config at the application root. The read, which runs later in the cache handler, resolves the same string from the application root and refuses to climb above it.
Using a bare web.config would succeed in the second stage, but it would fail during the first, and the first has to succeed for the second to be available to us. On the other hand, ../../../web.config succeeds in the first stage, but fails during the second. No relative path can satisfy both at the same time, and we seem to be stuck.
Anchoring the path with ~/
ASP.NET understands three kinds of paths: relative, rooted and application-relative. A relative path, such as web.config or ../web.config , is resolved from a base folder, and that base depends on the code doing the resolving, as our two stages just showed. A rooted path, such as /web.config , starts at the root of the website. An application-relative path starts with ~/ , and the ~ stands for the root of the current web application.
The website root and the application root are not always the same place. An application can sit at the root of a site, or under a virtual directory such as /shop/ . A rooted path only reaches the application’s files if you know where the application is installed, while ~/ works either way.
When ASP.NET’s path functions, such as VirtualPathUtility.ToAbsolute and MapPath , meet a path that starts with ~/ , they replace the ~ with the application’s virtual path. ~/web.config becomes /web.config for an application at the site root, and /shop/web.config for one under /shop/ . This is ASP.NET’s notation. IIS and browsers treat ~ as an ordinary character, so it only works where ASP.NET resolves the path on the server, and both of our stages do exactly that.
This is the behaviour we need to satisfy our competing constraints. The fetch and the read disagree which folder a relative path starts from, but an application-relative path does not start from either of them, so ~/web.config names the same file in both stages.
The fetch converts it in GetImageFromContentProvider , before the path reaches GetFile :
GetFile then receives an absolute path that already points into the application root, so the page’s folder no longer matters. The read needs nothing special, because MapPath treats a ~/ path as rooted at the application, whatever base folder it is given.
Both stages now land on the same file. We no longer need to know how deep the page sits, or whether the application is deployed under a virtual directory. It also works without a content provider, where the fetch never runs and the read still resolves ~/web.config to the root.
Now we run the attack again, this time anchoring our target file with ~/ . First we poison the cache using the same two requests, with imageUrl set to ~/web.config :
The fetch finds the file, and the response carries a new _downloadKey . We send it to the cache handler as before. This time the handler reads the file’s bytes and returns them, working out the response content type from the file’s own extension. It does not restrict which extensions it serves. For web.config the extension is config , so the file comes back as content type image/config :
The whole read, across both phases, looks like this:
The same read on any RadEditor page
Everything so far used a standalone RadImageEditor. A site built on Telerik UI for ASP.NET AJAX is not guaranteed to expose one that an unauthenticated user can reach. RadEditor, Telerik’s rich-text editor, stands to be far more common, and its built-in ImageEditor dialog opens another avenue to the same bug. The prerequisite is thus any reachable page that renders RadImageEditor or RadEditor.
On a RadEditor page the RadImageEditor is not on the page itself. It lives inside the ImageEditor dialog, which the browser loads on demand from Telerik.Web.UI.DialogHandler.aspx . To summon that dialog we have to provide the handler with its parameters, the encrypted dp blob. RadEditor writes a server-minted dp for each of its dialogs into the page HTML, so a GET returns them in the page body, and we harvest the ImageEditor dp and replay it unchanged.
How we set the image path changes slightly here too. On the standalone RadImageEditor, we used the imageUrl key in the client state to insert our path into the cache. In RadEditor’s ImageEditor dialog, we use the OriginalImageLocation form field for the same purpose. We send it alongside the encrypted dp that loads the ImageEditor dialog, and the dialog assigns it to the same server-side ImageUrl . That is the string stored in the cache once the fetch confirms the file exists.
From there the two routes converge. ImageUrl drives the cache poison, and we redeem the _downloadKey through the cache handler as before.
The field that lets the dialog take OriginalImageLocation is in ImageEditorDialog.cs :
The handler reads OriginalImageLocation from the form, normalises the separators, and assigns it to imgEditor.ImageUrl .
The dialog also switches the content provider on for its RadImageEditor, whatever the host page does:
With the switch on, the dialog runs the same fetch before it caches the path. As explained before for the standalone RadImageEditor route, the same applies here. The application-relative ~/web.config satisfies both the fetch during caching and the read that retrieves the file.
So the arbitrary file read on RadEditor is done in three steps:
GET any page that renders a RadEditor. Its RadDialogOpener bootstrap carries each dialog’s dp as SerializedParameters . We then capture the encrypted dp for the ImageEditor from the body.
POST the harvested dp back to Telerik.Web.UI.DialogHandler.aspx?DialogName=ImageEditor in the dialogParametersHolder form field, with OriginalImageLocation=~/web.config . The dialog loads and poisons the cache.
The response carries a _downloadKey bound to the poisoned entry. Read it back through the cache handler, exactly as before.
Important: Mind your query string length
For step 2, be sure to send dp in the form body rather than the query string. The blob is around 4 KB, and IIS gets cranky when you send a 4 KB query string. Interestingly, large query strings cause IIS to return a 404 response, which we think is a strange response for a request that is too large.
The request is an initial load, so there’s no need to worry sending __VIEWSTATE .
For more information, see the exploit’s README ↗ (opens in a new tab) .
Here is the dp , trimmed out of a GET response, inside the RadDialogOpener bootstrap:
And the replay that poisons the cache through the dialog:
The response is the dialog’s HTML, and the RadImageEditor bootstrap inside it carries a _downloadKey bound to the poisoned entry, just as the standalone page did:
From here the path rejoins the standalone read. We present that _downloadKey to the cache handler:
It answers with the same web.config , exactly as the standalone path did.
The whole read through the dialog handler looks like this:
From file read to code execution
As already covered, the arbitrary file read can only return files that resolve under the application root directory. Within this directory tree, web.config is a file that can hold exceptionally sensitive information. Depending on the secrets that have been provisioned within it, it may hold the keys to remote code execution.
web.config is the per-application configuration file. A bare default installation of Telerik usually carries nothing sensitive within it, but Telerik is just a UI component library, not an application on its own. A real application built on top of Telerik may contain one or more secrets, including:
Telerik.Web.UI.DialogParametersEncryptionKey
A static IIS machineKey value
Some of those secrets are enough to turn a disclosure of web.config into code execution.
An example of such a populated web.config file, cut down to the relevant settings, looks like this:
The machineKey entry carries a validationKey and a decryptionKey . It is the server-wide ASP.NET key that does important .NET things, such as signing and encrypting __VIEWSTATE , the state blob every WebForms page trusts when a postback returns it. If the application sets a static machineKey and you recover it, you can forge a __VIEWSTATE that carries a deserialisation gadget chain, a set of crafted objects that make the server run code while it reconstructs them from the blob, which is a long-standing route to code execution on a WebForms application.
Telerik.Web.UI.DialogParametersEncryptionKey is the key used by RadEditor’s dialog handler to encrypt and sign its dialog parameter blobs (the dp ). An adversary that holds this key can forge a file browser dialog whose upload folders point at the web root. Since the server trusts those configured directories without validation, we can turn the file browser into an unrestricted file upload and deploy a webshell. This is the DialogHandler UploadPaths Tampering Vulnerability ↗ (opens in a new tab) to which Progress Software assigned CVE-2026-19219.
Where Telerik.Web.UI.DialogParametersEncryptionKey is not set, the dialog handler instead uses the machineKey for encryption, so a static machineKey in the web.config also enables the same unrestricted file upload primitive.
Neither of these keys are set in web.config by default, but Telerik’s web.config setup guidance ↗ (opens in a new tab) strongly recommends that site owners explicitly set one or more of them. This guidance may encourage adoption of the settings, putting sites at greater risk should the contents of web.config become exposed.
RCE path one: forging a ViewState using the machineKey
The first path depends on the retrieved web.config file containing a machineKey value. With the machineKey in hand, an adversary can use the standard ASP.NET ViewState deserialisation technique to achieve RCE, as opposed to exploiting Telerik-specific functionality or behaviour. ASP.NET keeps page state in a hidden __VIEWSTATE field and trusts it as long as it is signed with the machineKey. We hold the key, so we can send a correctly signed __VIEWSTATE that carries a gadget chain, a nested set of objects whose deserialisation runs arbitrary code as a side effect.
This is what ysoserial.net ↗ (opens in a new tab) , the .NET deserialisation toolkit by Alvaro Muñoz (pwntester), is for.
Info: Porting ysoserial.net to Python
Running ysoserial.net gives you a ready-made serialised gadget chain to embed in a payload. To keep our exploit a single, self-contained script with no external dependencies, we ported its MIT-licensed implementation of the TextFormattingRunProperties gadget chain to Python, along with the ViewState key derivation it uses.
When the server deserialises a TextFormattingRunProperties object, it rebuilds one of its properties by parsing a XAML string with XamlReader.Parse . In our gadget chain, that XAML describes an ObjectDataProvider configured to call Process.Start . ObjectDataProvider invokes its configured method as soon as its properties are set, so deserialising the gadget chain is enough to trigger Process.Start .
In our exploit, we use this to write a webshell into the web root. It echoes the shell’s Base64 into a temp file, then decodes it with certutil -f -decode . The XAML, with the shell’s Base64 trimmed, looks like this:
Our exploit builds the __VIEWSTATE the same way ysoserial.net does. It serialises the gadget chain with BinaryFormatter, wraps it with LosFormatter (the serialiser ASP.NET uses for ViewState), then signs it with the machineKey, and encrypts it too when the target expects encryption.
ASP.NET has two ViewState crypto formats, selected by the httpRuntime targetFramework attribute in web.config . The differences come down to whether the keys are derived per page from the machineKey or the machineKey is used directly, whether encryption is mandatory or left to the page, and how the message authentication code that protects the ViewState from tampering is built.
None of this matters if you generate payloads with ysoserial.net, which supports both formats. It only matters if you reimplement parts of ysoserial.net, as we did to keep our exploit free of external dependencies. If you find yourself doing the same, ysoserial.net’s ViewState plugin ↗ (opens in a new tab) is the place to become familiar with those nuances.
Our exploit reads the httpRuntime targetFramework from the leaked web.config and checks the page’s __VIEWSTATEENCRYPTED field, then builds the matching format.
Then we post the forged __VIEWSTATE . If the attack succeeds, a 500 comes back and the webshell is written to the web root. When we later request the shell, its code runs as the worker process identity, the account the IIS application pool runs as.
RCE path two: forging the file browser upload paths using the dialog key
The second path depends on the retrieved web.config file containing a Telerik.Web.UI.DialogParametersEncryptionKey value. Unlike the first, this path abuses Telerik’s own functionality rather than standard ASP.NET behaviour. We forge the encrypted dialog parameters ( dp ) that tell RadEditor’s file browser where it may upload, point them at the web root, and upload a webshell straight into it.
The dp carries the file browser’s own configuration, including the folders it is allowed to browse ( ViewPaths ), upload to ( UploadPaths ) and delete from ( DeletePaths ).
DialogParametersSerializer.Deserialize decrypts it and accepts its fields as they were given. Nothing outside the blob constrains which folders it may refer to, so whichever paths a valid encrypted dp points to, the file browser uses.
The plaintext format of the configuration consists of a sequence of key,isArray,typeInt,value; records. This can be seen in the code snippet below, from DialogParametersSerializer.cs :
To forge one, we harvest a DocumentManager dp from a RadEditor page, decrypt it with the DialogParametersEncryptionKey value from web.config , rewrite the ViewPaths and UploadPaths arrays to point at the web root ( ~/ ), set SearchPatterns to *.* , raise MaxUploadFileSize , and re-encrypt.
UserControlFileBrowser copies the decrypted paths straight into the file browser configuration, and the client-supplied SearchPatterns becomes the upload extension filter, so *.* takes the constraint away.
The upload authorisation is only a path-prefix check against those same attacker-controlled UploadPaths . FileSystemContentProvider.CanUpload calls IsParentOf(uploadPaths[i], path) , and since the adversary set uploadPaths , the check passes for the web root. StoreFile then does no extension check:
Replaying the forged DocumentManager dp to DialogHandler.aspx?DialogName=DocumentManager , again in the dialogParametersHolder field, renders a RadFileExplorer (Telerik’s file browser control) scoped to the web root, which confirms the forge is accepted. The image below illustrates the attack in a browser:
Info: When uploads are blocked
The file browser stays useful when the upload fails, for example when the application pool identity cannot write to the web tree. Listing only needs read access, and since it lists the file system directly rather than going through IIS, it shows the entire web root.
The file browser itself has no way to download those files, but the arbitrary file read from earlier in this post can fetch any of them by name. Pairing a file listing primitive with the arbitrary file read allows an adversary to retrieve the application’s source, binaries and data, potentially exposing vulnerabilities in the code that consumes Telerik, as well as any user data stored in the web tree.
From there the file explorer’s async upload, which posts to WebResource.axd?type=rau , uploads the .aspx webshell into the web root. When we then request the shell, its code runs as the worker process identity.
The dialog crypto differs across Telerik versions. Versions from 2026.2.708 onwards use AES-GCM, while prior versions use AES-CBC, so our exploit automatically detects the right scheme by decrypting a real server-issued blob first.
Our exploit ↗ (opens in a new tab) runs the whole chain end-to-end. It reads web.config by exploiting CVE-2026-18672, then escalates to RCE through the upload path via CVE-2026-19219 or through the ViewState path, depending on which one the target’s configuration allows. If both are available, it defaults to ViewState.
The webshell it drops is self-encrypted and only decrypts its code at run time, mainly to avoid detection by signature-based antivirus. A random password is generated per run, and the shell self-deletes when the interactive session ends, unless --keep-shell is passed.
Here is the upload path against version 2026.2.708. The exploit reads web.config through the dialog handler, recovers the DialogParametersEncryptionKey , forges a DocumentManager dialog whose upload paths point at the web root, and uploads the shell through the file explorer.
The --dialog-key option supplies the DialogParametersEncryptionKey directly, skipping the web.config read. Anyone who holds the key can forge the parameters and upload a shell, whether they recovered it from the read, from source control, or from reuse across environments.
The ViewState path works the same way. Instead of forging a dialog, it forges a signed __VIEWSTATE whose gadget chain, once deserialised, writes the shell to disk through certutil . It needs a static machineKey in web.config , which the file read provides. The forge auto-detects which ViewState crypto the target uses: the .NET 4.5 format with SP800-108 derived keys, or the legacy format with raw machineKey bytes. It also detects whether the ViewState is encrypted or MAC-only.
The --rce-exec option runs a single command through the same gadget chain instead of dropping a shell. The forged ViewState then runs cmd.exe /c directly and writes nothing to disk. Deserialisation throws after Process.Start , so the command runs blind, with no output returned, which suits one-off actions rather than an interactive session.
To familiarise yourself with the full set of flags and how to use them, see the exploit’s README ↗ (opens in a new tab) .
The arbitrary file read has a distinctive footprint. A WebResource.axd?type=iec response whose Content-Disposition filename is not an image extension, or whose content type is something like image/config , is not normal traffic. You should alert on it and IIS logs for past type=iec requests, correlating response sizes against sensitive files such as web.config . For the code execution, you should watch process-creation logs for w3wp.exe spawning child processes such as cmd.exe , and use file-integrity monitoring on the web-accessible tree to catch unexpected .aspx files.
If you suspect that a web.config file has been obtained by an adversary, you should rotate every secret that it contains.
You should consider restricting the application pool identity so it cannot write anywhere in the web-accessible tree. Any folder that must stay writable, such as an upload folder, should sit outside it or have script execution disabled. That stops both routes from writing a webshell, but it does not stop command execution via ViewState deserialisation.
Progress Software fixed both issues in version 2026.3.812, the 2026 Q3 release.
For the file read, version 2026.3.812 adds two checks on the path the handler serves. The decrypted path is checked against the control’s configured image directories, and the handler that returns the bytes enforces an image extension allow-list of jpg , jpeg , png , gif and bmp . Either one stops the web.config read, and the dialog’s OriginalImageLocation now runs through the same checks.
Progress Software also tightened the existing application-root containment check, although we never bypassed that check, so we cannot say what prompted it.
As a further layer, RadImageEditor now stores its image location in the client state as an encrypted value under a new server key, so a client cannot substitute its own path without that key.
For the dialog upload, the fix has two parts: an extension denylist and an opt-in path restriction.
The first is a hardcoded denylist of dangerous extensions, applied when a file is written. It rejects .aspx , .ashx , .asax , .config , .exe , .dll and a long list of other executable and script types.
The second is a new web.config setting, Telerik.Web.UI.FileBrowserAllowedPaths , that restricts which folders RadEditor’s file browser and RadFileExplorer may list and upload to. It is opt-in rather than on by default. When it is not set, the file browser keeps its behaviour and trusts whatever ViewPaths and UploadPaths the encrypted dialog parameters carry. This means that an adversary who knows a site’s Telerik.Web.UI.DialogParametersEncryptionKey can still upload files to any folder on the site, including the web root.
Signing configuration and handing it to the client is a legitimate design. The problem was not the signing, but that a validly signed blob could name upload folders the developer never configured.
The coordinated disclosure timeline with Progress Software:
28 July 2026. Reported the findings to Progress Software via their security disclosure channel. Progress Software acknowledged receipt the same day.
31 July 2026. Progress Software provided a patched build for validation and review.
1 August 2026. We sent our feedback, confirming that the arbitrary file read was fixed but noting that the arbitrary file upload remained reachable where an adversary holds the DialogParametersEncryptionKey .
12 August 2026. Progress Software released the patched version, 2026.3.812 (2026 Q3), and urged customers to upgrade through a critical security bulletin.
2 September 2026. Progress Software published the CVEs and the associated knowledge-base advisories.
27 October 2026. We published this post.
This post concludes our research on Telerik UI for ASP.NET AJAX, at least for now. Across the two posts in this series, we reported 12 vulnerabilities, all now patched, and built two unauthenticated chains to remote code execution. This post discussed the RadImageEditor file read, and the post covered the RadAsyncUpload padding oracle.
In both posts, the security of these components came down to a handful of configuration values the server pushed to the client and trusted on return because they were encrypted.
Both chains relied on the same pattern. The server hands the client a signed or encrypted blob and trusts it when it comes back. This is common and legitimate. JSON Web Tokens, SAML assertions, framework session cookies, ASP.NET ViewState and cloud pre-signed URLs all work this way, because it lets the server keep no state of its own and validate the blob on each request instead of looking the client up.
The pattern’s safety rests on two things, that the key stays secret and that the crypto is sound. Each post took away one of them. In the first we broke the crypto with a padding oracle and forged values without the key. In this one we read the key off disk and forged values with it. Either way, the blob was no longer trustworthy.
The point specific to these two issues is narrower. When the blob carries a security decision, such as which folder a file is read from or written to, a valid signature only proves the blob was produced with the key, not that a developer intended those paths.
Progress Software has published an advisory for each of the vulnerabilities we discovered. We recommend that users of Telerik UI for ASP.NET AJAX read them, apply the necessary patches, and carefully consider Progress Software’s hardening recommendations.
RadAsyncUpload AsyncUploadTypeName Type Resolution Vulnerability (CVE-2026-13181) ↗ (opens in a new tab)
RadAsyncUpload Client-State Decrypt-vs-Parse Oracle Vulnerability (CVE-2026-13182) ↗ (opens in a new tab)
RadAsyncUpload Upload Metadata Timing Oracle Vulnerability (CVE-2026-13183) ↗ (opens in a new tab)
RadAsyncUpload Default HMAC Key Fallback Vulnerability (CVE-2026-13184) ↗ (opens in a new tab)
DialogHandler Provider Type Tampering Vulnerability (CVE-2026-13187) ↗ (opens in a new tab)
DialogHandler Parameters Tampering Vulnerability (CVE-2026-13188) ↗ (opens in a new tab)
SpellChecker DictionaryLanguage Path Traversal Vulnerability (CVE-2026-13189) ↗ (opens in a new tab)
RadEditor PDF Export SSRF Vulnerability (CVE-2026-13192) ↗ (opens in a new tab)
XXE Denial of Service via RadLayoutBuilder Client State (CVE-2026-14865) ↗ (opens in a new tab)
Unauthenticated File Read and Deletion via Hardcoded Encryption Key in RadChart (CVE-2026-14932) ↗ (opens in a new tab)
RadImageEditor Path Traversal Vulnerability (CVE-2026-18672) ↗ (opens in a new tab)
DialogHandler UploadPaths Tampering Vulnerability (CVE-2026-19219) ↗ (opens in a new tab)
You can find proof-of-concept exploits ↗ (opens in a new tab) for most of these issues on our GitHub.
If you run Telerik UI for ASP.NET AJAX, you should upgrade to version 2026.3.812 (2026 Q3) or later. That fixes the arbitrary file read, so web.config can no longer be read through this chain and the encryption keys in it cannot be leaked this way.
You should also consider setting Telerik.Web.UI.FileBrowserAllowedPaths in web.config to restrict which folders RadEditor’s file browser and RadFileExplorer can list and upload to. That restricts the upload path even if an adversary obtains the key by some other route, such as a copy committed to source control or reused across environments. It is opt-in and has no effect unless you set it. If there is any chance web.config was read, you should also consider rotating every secret in it.
We want to thank Progress Software for their commitment to security and for the work that went into these fixes. Coordinated disclosure only works when the vendor engages with it, and across both engagements Progress Software acknowledged our reports, worked through the detail with us, and shipped a fix for every issue we raised.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
