DevExtreme Angular - Security Considerations
This help topic describes potential security vulnerabilities and what you can do as an application developer to mitigate risks when using DevExtreme UI components.
HTML Encoding
HTML encoding is a simple technique that helps protect your web application from cross-site scripting (XSS) attacks. In an XSS attack, the threat actor injects a malicious script into your web application. Every time a user visits the impacted portion of the application, this script executes. To prevent code injection, user input must always be encoded (converted to plain text).
Encode User Input
DevExtreme text editors, such as TextBox, Autocomplete, and HtmlEditor, do not encode user input. We recommend that you apply third-party sanitizing tools to user input before submitting it to the server:
jQuery
$(function() { const editorInstance = $("#html-editor").dxHtmlEditor({ // ... }).dxHtmlEditor("instance"); $("#button").dxButton({ useSubmitBehavior: true, text: "Submit the Form" }); $("#form-container").on("submit", function(e) { const editorValue = editorInstance.option("value"); // ... // Encode editorValue here with your favorite sanitizing tool before sending this value to the server // ... e.preventDefault(); }); });
<form action="your-action" id="form-container"> <div id="html-editor"></div> <div id="button"></div> </form>
Angular
<form action="your-action" (submit)="onFormSubmit($event)"> <dx-html-editor ... [(value)]="editorValue"> </dx-html-editor> <dx-button [useSubmitBehavior]="true" text="Submit the Form"> </dx-button> </form>
import { Component, ViewChild } from '@angular/core'; import { DxHtmlEditorComponent } from 'devextreme-angular'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { editorValue = ''; onFormSubmit (e) { // ... // Encode this.editorValue here with your favorite sanitizing tool before sending this value to the server // ... e.preventDefault(); } }
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { DxHtmlEditorModule, DxButtonModule } from 'devextreme-angular'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, DxHtmlEditorModule, DxButtonModule ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
Vue
<template> <form action="your-action" @submit="handleSubmit"> <DxHtmlEditor ... v-model:value="editorValue" /> <DxButton :use-submit-behavior="true" text="Submit the Form" /> </form> </template> <script> import 'devextreme/dist/css/dx.light.css'; import { DxHtmlEditor } from 'devextreme-vue/html-editor'; import { DxButton } from 'devextreme-vue/button'; export default { components: { DxHtmlEditor, DxButton }, data: { editorValue: '' }, methods: { handleSubmit(e) { // ... // Encode this.editorValue here with your favorite sanitizing tool before sending this value to the server // ... e.preventDefault(); } } } </script>
React
import React, { useCallback, useState } from 'react'; import 'devextreme/dist/css/dx.light.css'; import { HtmlEditor } from 'devextreme-react/html-editor'; import { Button } from 'devextreme-react/button'; const App = () => { const [editorValue, setEditorValue] = useState(""); const onFormSubmit = useCallback((e) => { // ... // Encode editorValue here with your favorite sanitizing tool before sending this value to the server // ... e.preventDefault(); }, []); const handleValueChange = (e) => { setEditorValue(e.value); }; return ( <form action="your-action" onSubmit={onFormSubmit} <HtmlEditor ... value={editorValue} onValueChanged={handleValueChange} /> <Button useSubmitBehavior={true} text="Submit the Form" /> </form> ); } export default App;
customizeTooltip
The customizeTooltip callback function allows you to customize tooltips in SVG components. This function should return an object with individual tooltip settings. This object can include an html
field that accepts an HTML string. If this string contains JavaScript code, the UI component will execute it.
To guard against XSS attacks when using the customizeTooltip callback function, ensure that the html
value does not contain malicious code or use the text
field instead. Unlike html
values, text
values are encoded. Refer to the following example to see the difference:
encodeHtml
encodeHtml is a Boolean property that you can set for a column in the DataGrid, a column in the TreeList, a column in the Gantt, a cell in the PivotGrid, and a header filter value in the PivotGridFieldChooser. The default encodeHtml value is true
, and the component encodes corresponding values. If you set it to false
, the component disables encoding, and malicious code can be executed. To mitigate security-related risks, we recommend that you always set this property to true
.
To see what can occur if you disable the encodeHtml property open the following example:
In this example, the data source stores the malicious code:
const products = [{ "ProductID": 1, "ProductName": "<img src=1 onerror=alert('XSS') \/>", // ... }, { "ProductID": 2, "ProductName": "<script>alert('XSS')<\/script>", // ... }, // ... ];
When encodeHtml is true
, the DataGrid interprets this code as text and simply displays it:
If you set encodeHtml to false
, the malicious code will be interpreted as script, and you will see an alert pop-up window:
html
Items in DevExtreme collection UI components (List, SelectBox, Toolbar, and similar components) can apply appearance based on data source fields (see our Default Templates article for more information). html is one of the fields that specifies item markup. Values for this field are not encoded. As such, you must ensure that these values do not contain malicious code. Alternatively, you can use the text field. Unlike html values, text values are encoded.
The following example illustrates how improper use of the html field can create a vulnerability:
In this example, both text and html values contain unsafe HTML, but html lines are commented out:
const products = [{ "id": 1, "text": "<img src=1 onerror=alert('XSS') \/>", // "html": "<img src=1 onerror=alert('XSS') \/>" }, { "ID": 2, "text": "<script>alert('XSS')<\/script>", // "html": "<script>alert('XSS')<\/script>" }, { "id": 3, "text": "Product 1" // "html": "Product 1" }];
When html is commented out, text applies. You can see that its values are interpreted as text and simply displayed:
Uncomment the html lines, and you will see an alert pop-up window. This is because unsafe HTML was interpreted as a script and executed:
markers[].tooltip.text
The Map component can contain multiple markers. Each marker can display a tooltip. Its content is taken from the marker's tooltip.text property. This property can accept an HTML string – a string which is evaluated by the component. If the HTML contains JavaScript commands, it will be executed. The following example illustrates what can occur if appropriate measures are not taken: Map.markers[].tooltip.text - Potential XSS Vulnerability
If markers data is obtained from an untrusted source, encode your tooltip.text value as follows:
const markers = [ ... ]; const encodeMessage = (message) => { // ... // Encode the `message` string with your favorite sanitizing tool // ... return encodedMessage; }; const sanitizeMarkers = (markers) => { markers.forEach(marker => { marker.tooltip.text = encodeMessage(marker.tooltip.text); }) return markers; };
messageHtml
DevExtreme Dialog UI methods accept an unencoded HTML string as a dialog message. Encode this string in the following manner:
const message = "Are you sure?<script>alert('XSS')</script>"; // ... // Encode the `message` string with your favorite sanitizing tool // ... DevExpress.ui.dialog.confirm(messageEncoded, "Confirm changes");
This code produces the following output:
noDataText
The noDataText property specifies text to display when the UI component does not contain any data. This property can accept an HTML string – a string the component will evaluate. During evaluation, the noDataText property can make your app vulnerable to XSS attacks. If the noDataText value is obtained from a third party (loaded from a data source, entered by a user), encode this value to protect against threat actors/harmful code.
The following example illustrates how an unsafe noDataText value can affect your application:
You should encode the value as follows:
jQuery
const noDataTextUnsafe = "No data to display<img src=1 onerror=alert('XSS') \/>"; const encodeMessage = (message) => { // ... // Encode the `message` string with your favorite sanitizing tool // ... return encodedMessage; }; $(function() { $("#simpleList").dxList({ // ... noDataText: encodeMessage(noDataTextUnsafe) }); });
Angular
<dx-list ... [noDataText]="noDataTextEncoded"> </dx-list>
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { noDataTextEncoded: string; constructor() { const noDataTextUnsafe = "No data to display<img src=1 onerror=alert('XSS') \/>"; this.noDataTextEncoded = this.encodeMessage(noDataTextUnsafe); } encodeMessage (message) { // ... // Encode the `message` string with your favorite sanitizing tool // ... return encodedMessage; }; }
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { DxListModule } from 'devextreme-angular'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, DxListModule ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
Vue
<template> <DxList ... :no-data-text="noDataTextEncoded" /> </template> <script> import 'devextreme/dist/css/dx.light.css'; import DxList from 'devextreme-vue/list'; const noDataTextUnsafe = "No data to display<img src=1 onerror=alert('XSS') \/>"; const encodeMessage = (message) => { // ... // Encode the `message` string with your favorite sanitizing tool // ... return encodedMessage; }; export default { components: { DxList }, data() { return { // ... noDataTextEncoded: encodeMessage(noDataTextUnsafe) } } } </script>
React
import React, { useState } from 'react'; import 'devextreme/dist/css/dx.light.css'; import List from 'devextreme-react/list'; const encodeMessage = (message) => { // ... // Encode the `message` string with your favorite sanitizing tool // ... return encodedMessage; }; export default function App() { const noDataTextUnsafe = "No data to display<img src=1 onerror=alert('XSS') \/>"; const noDataTextEncoded = encodeMessage(noDataTextUnsafe); return ( <List ... noDataText={noDataTextEncoded} /> ); }
CSV Injection
If you export data from the DevExtreme DataGrid or PivotGrid in CSV format, your app may be vulnerable to a CSV Injection Attack (also known as a formula injection attack) unless appropriate measures are taken. CSV Injection Attacks involve the injection of a malicious character sequence that is interpreted as a formula and executed within a computer network. Cell values that start with =, +, -, and @ characters can initiate an injection attack.
When executed, malicious code in a formula can alter user data, or allow unauthorized access to data or internal resources.
You can encode CSV files to prevent execution of potentially harmful code within them. Pass the encodeExecutableContent option as an argument of the configuration object of the exportDataGrid or exportPivotGrid function.
jQuery
$(function() { $("#dataGridContainer").dxDataGrid({ // ... export: { enabled: true, formats: ['csv'] }, onExporting(e) { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('Employees'); DevExpress.excelExporter.exportDataGrid({ component: e.component, worksheet: worksheet, encodeExecutableContent: true, }).then(() => { workbook.csv.writeBuffer().then((buffer) => { saveAs(new Blob([buffer], { type: 'application/octet-stream' }), 'DataGrid.csv'); }); }); } }); });
<head> <!-- ... --> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.4.0/polyfill.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/3.3.1/exceljs.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script> <!-- reference the DevExtreme sources here --> </head>
Angular
<dx-data-grid ... (onExporting)="onExporting($event)"> <dxo-export [enabled]="true" [formats]="['csv']" ></dxo-export> </dx-data-grid>
import { Component } from '@angular/core'; import { exportDataGrid } from 'devextreme/excel_exporter'; import { Workbook } from 'exceljs'; import saveAs from 'file-saver'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { onExporting(e) { const workbook = new Workbook(); const worksheet = workbook.addWorksheet('Employees'); exportDataGrid({ component: e.component, worksheet: worksheet, encodeExecutableContent: true, }).then(() => { workbook.csv.writeBuffer().then((buffer) => { saveAs(new Blob([buffer], { type: 'application/octet-stream' }), 'DataGrid.csv'); }); }); } }
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { DxDataGridModule } from 'devextreme-angular'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, DxDataGridModule ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
Vue
<template> <DxDataGrid ... @exporting="onExporting"> <DxExport :enabled="true" :formats="['csv']" /> </DxDataGrid> </template> <script> // ... import { DxDataGrid, DxExport } from 'devextreme-vue/data-grid'; import { exportDataGrid } from 'devextreme/excel_exporter'; import { Workbook } from 'exceljs'; import saveAs from 'file-saver'; export default { components: { DxDataGrid, DxExport }, methods: { onExporting(e) { const workbook = new Workbook(); const worksheet = workbook.addWorksheet('Employees'); exportDataGrid({ component: e.component, worksheet: worksheet, encodeExecutableContent: true, }).then(function() { workbook.csv.writeBuffer().then((buffer) => { saveAs(new Blob([buffer], { type: 'application/octet-stream' }), 'DataGrid.csv'); }); }); } } } </script>
React
import React, { useCallback } from 'react'; // ... import DataGrid, { Export } from 'devextreme-react/data-grid'; import { Workbook } from 'exceljs'; import saveAs from 'file-saver'; import { exportDataGrid } from 'devextreme/excel_exporter'; const App = () => { const onExporting = useCallback((e) => { const workbook = new Workbook(); const worksheet = workbook.addWorksheet('Employees'); exportDataGrid({ component: e.component, worksheet: worksheet, encodeExecutableContent: true, }).then(function() { workbook.csv.writeBuffer().then((buffer) => { saveAs(new Blob([buffer], { type: 'application/octet-stream' }), 'DataGrid.csv'); }); }); }, []); return ( <DataGrid ... onExporting={onExporting}> <Export enabled={true} formats={['csv']} /> </DataGrid> ); } export default App;
ExcelJS CSP Threats
The DevExtreme DataGrid and PivotGrid components use the ExcelJS third-party library to export data to Excel. If you implement export functionality and apply CSP rules, you should include the following initialization code before code that loads ExcelJS sources: window.regeneratorRuntime = null;
jQuery
<head> <!-- ... --> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-uqcb8z' cdnjs.cloudflare.com;" /> <script nonce="uqcb8z"> window.regeneratorRuntime = null; </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/3.3.1/exceljs.min.js"></script> <!-- reference the DevExtreme sources here --> </head>
$(function(){ const dataGrid = $('#gridContainer').dxDataGrid({ // ... export: { enabled: true, formats: ['xlsx'], }, onExporting(e) { if (e.format === 'xlsx') { // ... } }, }).dxDataGrid('instance'); });
Angular
<head> <!-- ... --> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-uqcb8z' cdnjs.cloudflare.com;" /> <script nonce="uqcb8z"> window.regeneratorRuntime = null; </script> </head> <!-- ... -->
<dx-data-grid ... (onExporting)="onExporting($event)" > <!-- ... --> <dxo-export [enabled]="true" [formats]="['xlsx']" ></dxo-export> </dx-data-grid>
import { Component } from '@angular/core'; import { Workbook } from 'exceljs'; import { exportDataGrid } from 'devextreme/excel_exporter'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { onExporting(e) { if (e.format === 'xlsx') { // ... } } }
Vue
<head> <!-- ... --> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-uqcb8z' cdnjs.cloudflare.com;" /> <script nonce="uqcb8z"> window.regeneratorRuntime = null; </script> </head> <!-- ... -->
<template> <div> <DxDataGrid ... @exporting="onExporting" > <!-- ... --> <DxExport :enabled="true" :formats="['xlsx']" /> </DxDataGrid> </div> </template> <script> import 'devextreme/dist/css/dx.light.css'; import { DxDataGrid, DxExport } from 'devextreme-vue/data-grid'; import { Workbook } from 'exceljs'; export default { components: { DxDataGrid } data() { return { // ... }; }, methods: { onExporting(e) { if (e.format === 'xlsx') { // ... } }, }, } </script>
<template> <div> <DxDataGrid ... @exporting="onExporting" > <!-- ... --> <DxExport :enabled="true" :formats="['xlsx']" /> </DxDataGrid> </div> </template> <script setup> import 'devextreme/dist/css/dx.light.css'; import { DxDataGrid, DxExport } from 'devextreme-vue/data-grid'; import { Workbook } from 'exceljs'; const onExporting = (e) => { if (e.format === 'xlsx') { // ... } } </script>
React
<head> <!-- ... --> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-uqcb8z' cdnjs.cloudflare.com;" /> <script nonce="uqcb8z"> window.regeneratorRuntime = null; </script> </head> <!-- ... -->
import React from 'react'; import 'devextreme/dist/css/dx.light.css'; import DataGrid, { Export } from 'devextreme-react/data-grid'; import { Workbook } from 'exceljs'; const exportFormats = ['xlsx']; export default function App() { const onExporting = React.useCallback((e) => { if (e.format === 'xlsx') { // ... } }); return ( <React.Fragment> <div> <DataGrid ... onExporting={onExporting} > {/* ... */} <Export enabled={true} formats={exportFormats}> </DataGrid> </div> </React.Fragment> ); }
Content Security Policy
Content Security Policy (CSP) is a security feature that helps detect and mitigate certain types of attacks, such as clickjacking, cross-site scripting (XSS), and other malicious code injection attacks. The impact of such attacks varies but can include data theft, page spoofing, malware distribution, site defacement, etc. It is important to ensure that DevExtreme UI components are compatible with CSP.
If you want to apply CSP rules, define a <meta>
tag and configure your policy. For DevExtreme UI components, you can specify the following set of directives:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://* data:; child-src 'none';" />
Common CSP Directives
You can specify a set of CSP rules to define resources that your site allows or restricts. The following table lists common CSP directives and source values:
Directive | Example | Description |
---|---|---|
default-src |
default-src 'self' cdn.example.com; |
Default policy. Allow everything but only from the same origin. |
script-src |
script-src 'self' js.example.com; |
Only allow scripts from the same origin. |
style-src |
style-src 'self' css.example.com; |
Defines authorized sources for stylesheets (CSS). |
object-src |
object-src 'self'; |
Defines authorized sources for plugins (for example, <object>, <embed> or <applet>). |
img-src |
img-src 'self' img.example.com; |
Defines authorized sources for images, or link element related to an image type (for example, rel="icon"). |
frame-src |
frame-src 'self'; |
Defines authorized sources for loading frames (iframe or frame). |
form-action |
form-action 'self'; |
Defines valid sources that can be used as an HTML <form> action. |
CSP Source List
All directives ending with -src supply corresponding values referred to as a source list. You can separate multiple source list values with a space, except for the none
value, which should be used on its own.
Source Value | Example | Description |
---|---|---|
* |
img-src * |
Wildcard, allows any URL except "data:", "blob:", and "filesystem:" schemes. |
'none' |
object-src 'none' |
Rejects resources from any source. |
'self' |
script-src 'self' |
Allows a web site to load resources from the same origin (same scheme, host and port). |
data: |
img-src 'self' data: |
Allows a web site to load resources via the data scheme (for example, Base64 encoded images). |
domain.example.com |
img-src domain. example.com |
Allows a web site to load resources from the specified domain name. |
*.example.com |
img-src *.example.com |
Allows a web site to load resources from any subdomain under example.com . |
https://cdn.com |
img-src https://cdn.com |
Allows a web site to load resources only from the specified domain, and only over HTTPS. |
https: |
img-src https: |
Allows a web site to load resources from any domain, but only over HTTPS. |
'unsafe-inline' |
script-src 'unsafe-inline' |
Allows a web site to use inline source elements such as style attribute, onclick , or script tag bodies (depends on the context of the source it is applied to) and javascript: URIs. |
'unsafe-eval' |
script-src 'unsafe-eval' |
Allows a web site to perform unsafe dynamic code evaluation such as JavaScript eval() . |
'nonce-' |
script-src 'nonce-rAnd0m' |
Allows an inline script or CSS to execute if the script (for example, <script nonce="rAnd0m"> ) tag contains a nonce attribute matching the nonce specifed in the CSP header. The nonce should be a secure random string, and should not be reused. CSP Level 2 |
'strict-dynamic' |
script-src 'strict-dynamic' |
Enables an allowed script to load additional scripts via non-"parser-inserted" script elements (for example, document.createElement('script'); is allowed). CSP Level 3 |
'unsafe-hashes' |
script-src 'unsafe-hashes' |
Allows you to enable scripts in event handlers (for example, onclick ). Does not apply to javascript: or inline <script> . CSP Level 3 |
CSP Directives for Map Integrations
If you want to integrate DevExtreme UI components with Google and Bing maps API, specify the following set of CSP directives:
// Bing maps <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://*.bing.com https://*.virtualearth.net https://ssl.gstatic.com; style-src 'self' 'unsafe-inline' https://*.bing.com https://*.virtualearth.net; img-src 'self' data: https://*.bing.com https://*.virtualearth.net; font-src 'self' data:; connect-src 'self' https://*.bing.com" /> // Google maps <meta http-equiv="Content-Security-Policy" content="default-src 'self' https://*.googleapis.com/ https://*.gstatic.com; script-src 'unsafe-inline' 'unsafe-eval' 'self' https://*.googleapis.com/ https://*.gstatic.com; style-src 'unsafe-inline' 'self' https://*.googleapis.com/ https://*.gstatic.com; img-src 'self' data: https://*.googleapis.com/ https://*.gstatic.com;" />
CSP Directives for Themes and Icons
Material and Fluent (all trademarks or registered trademarks are property of their respective owners) themes do not support the default-src 'self'
directive out of the box. To enable this directive, export such themes from DevExtreme ThemeBuilder and remove links to external fonts.
You can also use ThemeBuilder CLI to export the theme:
// Fluent theme npx devextreme-cli build-theme --base-theme="fluent.blue.light" --remove-external-resources // Material theme npx devextreme-cli build-theme --base-theme="material.blue.light" --remove-external-resources
img-src data:
attribute is required to display custom SVG-based icons in certain components.If you have technical questions, please create a support ticket in the DevExpress Support Center.