Hi,
I am contacting you today to let you know I have found cross-site scripting vectors within the latest version of the RadEditor. I have attached images of the payloads that seem to bypass the XSS filter.
The second payload only works on Firefox browsers, but the first works on Chrome browsers too. While it still requires users to click on the link to trigger XSS, it can be easily social engineered in most situations.
Hi Norman,
The payloads demonstrated by Andrew are indeed not mitigated by the default XSS-stripping mechanisms provided by RadEditor:
As detailed in Peter Milchev’s response, mitigating these vectors requires additional measures beyond the provided filters. The recommended approach is to implement a custom client-side filter alongside server-side sanitization. Here’s a summary of the remediation steps:
Server-Side Content Sanitization
Below is an example of how you can sanitize the content on the server side by removing potentially harmful SVG attributes (to and href) that reference javascript:
C#
protected void Page_Load(object sender, EventArgs e)
{
string content = RadEditor1.Content;
// Regular expression to identify 'javascript:' in `to` and `href` attributes within SVG elements
string pattern = @"(<(set|animate|brute)[^>]*?\s(?:to|href)\s*=\s*['""]?)javascript:[^'""]*";
// Replace occurrences of 'javascript:' with an empty string
content = System.Text.RegularExpressions.Regex.Replace(content, pattern, "$1", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
// Assign the sanitized content back to the editor
RadEditor1.Content = content;
}
VB.NET
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
' Get the content from RadEditor
Dim content As String = RadEditor1.Content
' Regular expression to identify 'javascript:' in `to` and `href` attributes within SVG elements
Dim pattern As String = "(<(set|animate|brute)[^>]*?\s(?:to|href)\s*=['""]?)javascript:[^'""]*"
' Replace occurrences of 'javascript:' with an empty string
content = System.Text.RegularExpressions.Regex.Replace(content, pattern, "$1", System.Text.RegularExpressions.RegexOptions.IgnoreCase)
' Assign the sanitized content back to the editor
RadEditor1.Content = content
End Sub
Here is an improved version that matches tags with special attributes that can potentially execute JavaScript
protected void Page_Load(object sender, EventArgs e)
{
string content = RadEditor1.Content;
// Matches tags and attributes that can potentially execute JavaScript
string pattern = @"(<([a-zA-Z0-9]+)[^>]*?\s(?:to|href|xlink:href|src|action|formaction|style)\s*=\s*['""]?)javascript:[^'""]*";
// Replace occurrences of 'javascript:' with an empty string
content = System.Text.RegularExpressions.Regex.Replace(content, pattern, "$1", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
// Assign the sanitized content back to the editor
RadEditor1.Content = content;
}
VB.NET
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim content As String = RadEditor1.Content
' Matches tags and attributes that can potentially execute JavaScript
Dim pattern As String = "(<([a-zA-Z0-9]+)[^>]*?\s(?:to|href|xlink:href|src|action|formaction|style)\s*=\s*['""]?)javascript:[^'""]*"
' Replace occurrences of 'javascript:' with an empty string
content = System.Text.RegularExpressions.Regex.Replace(content, pattern, "$1", System.Text.RegularExpressions.RegexOptions.IgnoreCase)
' Assign the sanitized content back to the editor
RadEditor1.Content = content
End Sub
Client-Side Custom Filter
Implementing a client-side filter can help mitigate such issues before the content is posted back to the server. Here is an example of how a custom filter can be added to strip problematic SVG tags and attributes.
<telerik:RadEditor ID="RadEditor1" runat="server" OnClientLoad="OnClientLoad">
<Content>
<a href="javascript:alert('XSS')">Click me</a>
<svg><use xlink:href="javascript:alert('XSS')"></use></svg>
<img src="javascript:alert('XSS')" />
<button formaction="javascript:alert('XSS')">Submit</button>
<form action="javascript:alert('XSS')"></form>
<svg>
<set attributeName="href" to="javascript:alert('XSS')" />
<animate attributeName="to" to="javascript:alert('XSS')" />
</svg>
<custom-tag href="javascript:alert('XSS')">Custom</custom-tag>
<math><brute href="javascript:alert(1)">click</brute></math>
<svg><a><rect width="99%" height="99%"></rect><set attributename="href" to="javascript: alert(1)"></set></a></svg>
</Content>
</telerik:RadEditor>
<script type="text/javascript">
function OnClientLoad(editor, args) {
editor.get_filtersManager().add(new MyCustomSVGFilter());
}
function MyCustomSVGFilter() {
MyCustomSVGFilter.initializeBase(this);
this.set_isDom(true);
this.set_enabled(true);
this.set_name("MyCustomSVGFilter");
this.set_description("Strips potentially dangerous attributes that contain 'javascript:' or other harmful values.");
}
MyCustomSVGFilter.prototype = {
encodeScripts: function (contentToEncode) {
// List of tags to inspect for dangerous attributes
const tags = [
"a", "svg", "use", "img", "div", "button", "form",
"set", "animate", "custom-tag", "brute"
];
// Attributes to check for harmful values
const attributes = [
"href", "xlink:href", "src", "to", "formaction", "action", "style"
];
for (let tag of tags) {
let elements = contentToEncode.getElementsByTagName(tag);
for (let element of elements) {
for (let attr of attributes) {
let value = element.getAttribute(attr);
if (value && this.isDangerous(value)) {
element.removeAttribute(attr);
}
}
}
}
return contentToEncode;
},
isDangerous: function (value) {
// Identify dangerous values (e.g., starting with "javascript:")
value = value.toLowerCase();
return value.startsWith("javascript:") || value.includes("expression(");
},
getDesignContent: function (content) {
return this.encodeScripts(content);
},
// The same logic applies when switching to HTML mode or submitting content
getHtmlContent: function (content) {
return this.encodeScripts(content);
}
};
MyCustomSVGFilter.registerClass('MyCustomSVGFilter', Telerik.Web.UI.Editor.Filter);
</script>
Regards,
Rumen
Progress Telerik
DefaultFilters,StripCssExpressions,StripDomEventAttributes,RemoveScripts
"Hi Sullivanst,
Thank you for pointing this out, the old check indeed had this flaw. I have updated the code so it first makes the attribute lower case and then searches with indexOf:
if (attr && attr.toLowerCase().indexOf("javascript") > -1) {
Regards,
Peter Milchev
Progress Telerik
Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.
Hello Andrew,
Thank you for bringing up this case, it indeed is not sanitized by the DOM or events sanitizers. The same issue is observed also with the <animate> tag in the SVG as well:
For the time being, you can alleviate the issue by using a custom filter client-side and manually sanitizing the content on the server-side:
Here is a sample implementation of the client-side filter:
<script type="text/javascript">
function OnClientLoad(editor, args) {
editor.get_filtersManager().add(new MyCustomSVGFilter());
}
function MyCustomSVGFilter() {
MyCustomSVGFilter.initializeBase(this);
this.set_isDom(true);
this.set_enabled(true);
this.set_name("MyCustomSVGFilter");
this.set_description("Strips set, animate and brute attributes that contain 'javascript:' as value");
}
MyCustomSVGFilter.prototype = {
encodeScripts: function (contentToEncode) {
var list = contentToEncode.getElementsByTagName("set");
for (let item of list) {
var attr = item.getAttribute("to");
if (attr && attr.toLowerCase().indexOf("javascript") > -1) {
item.removeAttribute("to");
}
}
list = contentToEncode.getElementsByTagName("animate");
for (let item of list) {
var attr = item.getAttribute("to");
if (attr && attr.toLowerCase().indexOf("javascript") > -1) {
item.removeAttribute("to");
}
}
list = contentToEncode.getElementsByTagName("brute");
for (let item of list) {
var attr = item.getAttribute("href");
if (attr && attr.toLowerCase().indexOf("javascript") > -1) {
item.removeAttribute("href");
}
}
return contentToEncode;
},
getDesignContent: function (content) {
return this.encodeScripts(content);
},
// The code below is the same because it needs to be applied when switching to HTML mode and also when content is submitted.
getHtmlContent: function (content) {
return this.encodeScripts(content);
}
}
MyCustomSVGFilter.registerClass('MyCustomSVGFilterFilter', Telerik.Web.UI.Editor.Filter);
</script>
As a small token of gratitude for helping us identify this, we have updated your Telerik points.
Regards,
Peter Milchev
Progress Telerik
Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.