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 rumen,
thank you very much!
Kris
Hi Kris,
Thank you for your feature request! While we are still researching how this can be implemented in RadEditor, you can enhance your current solution by sanitizing the HTML content during typing, for example in the textarea's keydown event:
<telerik:RadEditor runat="server" ID="RadEditor1" Height="600px" OnClientModeChange="OnClientModeChange" ContentFilters="DefaultFilters,StripDomEventAttributes">
<Content>
</Content>
<Modules>
<telerik:EditorModule Name="RadEditorStatistics" Visible="true" />
<telerik:EditorModule Name="RadEditorDomInspector" Visible="true" />
<telerik:EditorModule Name="RadEditorNodeInspector" Visible="false" />
<telerik:EditorModule Name="RadEditorHtmlInspector" Visible="true" />
</Modules>
</telerik:RadEditor>
<script type="text/javascript">
function OnClientModeChange(editor, args) {
// Check if the editor is switching to Html mode (mode 2)
if (editor.get_mode() === 2) {
var textArea = editor.get_textArea();
if (textArea) {
// Attach the paste event handler to the textarea
textArea.addEventListener('paste', handlePaste);
// Attach the keydown event handler to the textarea
textArea.addEventListener('keydown', handleKeydown);
}
} else {
// Clean up the event listeners when switching away from Html mode
var textArea = editor.get_textArea();
if (textArea) {
textArea.removeEventListener('paste', handlePaste);
textArea.removeEventListener('keydown', handleKeydown);
}
}
}
function sanitizeHtmlAttributes(html) {
// Remove on... event attributes (e.g., onload, onclick, etc.)
return html.replace(/\s+on\w+\s*=\s*(['"]?)[^ >]*\1/gi, "");
}
function handlePaste(event) {
event.preventDefault();
var clipboardData = event.clipboardData || window.clipboardData;
var pastedData = clipboardData.getData('text/plain');
var sanitizedData = sanitizeHtmlAttributes(pastedData);
document.execCommand('insertText', false, sanitizedData);
}
function handleKeydown(event) {
// Use setTimeout to wait for the value to update after the key press
var textArea = event.target;
setTimeout(function () {
var sanitized = sanitizeHtmlAttributes(textArea.value);
if (sanitized !== textArea.value) {
var pos = textArea.selectionStart;
textArea.value = sanitized;
// Optionally, restore cursor position
textArea.setSelectionRange(pos, pos);
}
}, 0);
}
</script>
Regards,
Rumen
Progress Telerik
Hi Rumen,
thank you for the suggestion with paste handler, we'll use it for sure. This won't help in case if someone types malicious code though. I check other similar editors and the issue does not happen there. Any chance this will be fixed?
All best,
Kris
Hi Kris,
Thank you for the detailed reproduction steps and your accurate analysis.
You've correctly identified that when pasting HTML containing an onload attribute into the HTML view the browser executes the script. This happens because the browser's rendering engine processes the content immediately upon switching modes.
While this scenario requires a user to knowingly paste the code themselves, it is best to prevent any script execution proactively. The most effective solution is to sanitize the content at the moment it is pasted into the HTML view.
The implementation below uses the OnClientModeChange event to attach a paste handler to the editor's HTML textarea. This handler intercepts the pasted content, strips any on... event attributes, and then inserts the sanitized version, preventing the browser from ever executing the script.
Here is the recommended code:
<telerik:RadEditor runat="server" ID="RadEditor1" Height="600px" OnClientModeChange="OnClientModeChange" ContentFilters="DefaultFilters,StripDomEventAttributes">
<Content>
</Content>
<Modules>
<telerik:EditorModule Name="RadEditorStatistics" Visible="true" />
<telerik:EditorModule Name="RadEditorDomInspector" Visible="true" />
<telerik:EditorModule Name="RadEditorNodeInspector" Visible="false" />
<telerik:EditorModule Name="RadEditorHtmlInspector" Visible="true" />
</Modules>
</telerik:RadEditor>
<script type="text/javascript">
function OnClientModeChange(editor, args) {
// Check if the editor is switching to Html mode (mode 2)
if (editor.get_mode() === 2) {
var textArea = editor.get_textArea();
if (textArea) {
// Attach the paste event handler to the textarea
textArea.addEventListener('paste', handlePaste);
}
} else {
// Clean up the event listener when switching away from Html mode
var textArea = editor.get_textArea();
if (textArea) {
textArea.removeEventListener('paste', handlePaste);
}
}
}
function handlePaste(event) {
// Prevent the default paste action to take control
event.preventDefault();
// Get the plain text content from the clipboard
var clipboardData = event.clipboardData || window.clipboardData;
var pastedData = clipboardData.getData('text/plain');
// Sanitize the pasted data by removing on... event attributes
var sanitizedData = pastedData.replace(/\s+on\w+\s*=\s*[^ >]+/gi, "");
// Use execCommand to insert the sanitized text.
// This is a reliable way to handle pasting in content-editable areas and textareas.
document.execCommand('insertText', false, sanitizedData);
}
</script>
This approach ensures that no malicious script is ever present in the editor's content area, effectively mitigating the vulnerability before the browser can act on it.
Alternatively, you can hide the HTML mode, by setting EditModes="Design".
Regards,
Rumen
Progress Telerik
Hi Rumen,
here are the steps to reproduceL
1. Insert the editor on a page, keep the content empty:
<telerik:RadEditor runat="server" ID="RadEditor1" Height="600px" ContentFilters="DefaultFilters,StripDomEventAttributes">
<Modules>
<telerik:EditorModule Name="RadEditorStatistics" Visible="true" />
<telerik:EditorModule Name="RadEditorDomInspector" Visible="true" />
<telerik:EditorModule Name="RadEditorNodeInspector" Visible="false" />
<telerik:EditorModule Name="RadEditorHtmlInspector" Visible="true" />
</Modules>
</telerik:RadEditor>
2. Load the page
3. Switch to the HTML tab
4. Paste the code:
<svg/onload=alert(1)><svg> <svg onload=alert(1)><svg> # newline char <svg onload=alert(1)><svg> # tab char <svgonload=alert(1)><svg> # new page char (0xc)
5. Switch to Design view
6. Watch alerts ;-)
Kris
Hi Krzysztof,
I recorded the following video https://youtu.be/ls4rwtNYp18 showing that the StripDomEventAttributes does strip the onload attributes. Can you please examine your configuration and ensure that the filter is enabled.
<telerik:RadEditor runat="server" ID="RadEditor1" Height="600px" ContentFilters="DefaultFilters,StripDomEventAttributes">
<Content>
<svg
onload=alert(1)><svg> # newline char
<svg onload=alert(1)><svg> # tab char
<svgonload=alert(1)><svg> # new page char (0xc)
</Content>
<Modules>
<telerik:EditorModule Name="RadEditorStatistics" Visible="true" />
<telerik:EditorModule Name="RadEditorDomInspector" Visible="true" />
<telerik:EditorModule Name="RadEditorNodeInspector" Visible="false" />
<telerik:EditorModule Name="RadEditorHtmlInspector" Visible="true" />
</Modules>
</telerik:RadEditor>
If you still experience the problem, please provide reproduction steps so that I can reproduce it.
Regards,
Rumen
Progress Telerik
Hi Rumen,
I use the StripDomEventAttributes content filter, and in general it is working ok - but not with the code I sent below. Can you please check on your end?
Thank you!
<svg onload=alert(1)><svg> # newline char <svg onload=alert(1)><svg> # tab char <svgonload=alert(1)><svg> # new page char (0xc)
Hi Krzysztof,
You can remove the onload and other dangerous attributes by enabling the StripDomEventAttributes anti-XSS content filter of RadEditor, e.g. ContentFilters="DefaultFilters,StripDomEventAttributes".
You can find more information about it at:
Regards,
Rumen
Progress Telerik
Hi,
we have tested to custom filter but found an issue - for the code below:
<svg onload=alert(1)><svg> # newline char <svg onload=alert(1)><svg> # tab char <svgonload=alert(1)><svg> # new page char (0xc)
alerts are firing before the custom filter function. Is there a way to fix it?
Regards
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/.