Elementor allows you to insert values from custom fields, post metadata, URL parameters, comments, and user data into pages. This is convenient for dynamic templates, but it becomes dangerous when a value reaches HTML, an attribute, or JavaScript without validation.
An XSS (Cross-Site Scripting) attack occurs when an attacker causes a website to execute code they control in another visitor’s browser. Consequences can include stealing data from the current session, replacing page content, displaying phishing forms, or performing actions on behalf of the user. Below is a practical approach to securing Elementor, custom widgets, shortcodes, and WordPress code.
Where XSS Risks Arise in Elementor
The danger does not come from using dynamic tags itself, but from handling data incorrectly as it travels from its source to the browser. A value may be entered by an administrator or post author, submitted by a visitor through a form, or passed in a URL. If it is rendered in an unsafe context, the browser may interpret part of the string as HTML or JavaScript.
Pay particular attention to custom solutions: PHP templates, shortcodes, custom Elementor widgets, field integrations, and JavaScript that reads URL parameters. A standard widget may have its own processing, but that does not mean every surrounding piece of code is safe.
- text from custom fields and post metadata;
- values from comments, forms, and user profiles;
- URL parameters, such as a search query or product identifier;
- dynamic values in href, src, title, class, or data-* attributes;
- HTML widgets, Custom Code, and JavaScript insertions;
- custom shortcodes and widgets that use echo without context-specific escaping.
Sanitization and Escaping: What Is the Difference?
Sanitization cleans or normalizes data before it is saved. For example, sanitize_text_field() can be used for a short text field, while esc_url_raw() can be used for a URL during saving. This is a useful layer of protection, but it does not replace escaping during output.
Escaping depends on where the value will be used. The same string cannot be inserted safely in the same way into text, an HTML attribute, a URL, and JavaScript. The basic rule is: validate data when receiving and saving it, but escape it immediately before every output.
Do not assume that a field is already clean because it was entered in the admin area or processed by a plugin. Data may arrive through another path, change after being saved, or be used in a different context.
- sanitize_text_field() — for simple text values during processing or saving;
- sanitize_email() — for email addresses;
- absint() or an allowlist check — for numbers and identifiers;
- esc_html() — for ordinary text inside HTML;
- esc_attr() — for HTML attribute values;
- esc_url() — for URLs rendered on the page;
- wp_kses_post() — when limited safe HTML needs to be allowed.
Correct Escaping for Different Contexts
Choose the function based on where the value is rendered, not on the field’s name. For example, a field called “Title” may be rendered as text in a heading, as a title attribute, or as part of a link. Each case requires separate escaping.
For ordinary text, do not use wp_kses_post() when HTML is not needed. It is safer to output the string as text with esc_html(). If the business logic genuinely requires formatting, allow only a limited set of HTML tags through wp_kses_post() or a custom list of permitted tags.
- Text in markup: esc_html($value).
- Attribute: esc_attr($value).
- Link: esc_url($url).
- Limited HTML: wp_kses_post($html).
- Numeric value: absint($value) or range validation.
- Value for JavaScript: do not insert it through simple string concatenation; use safe data transfer, such as wp_json_encode(), with the context taken into account.
<?php
$title = get_post_meta( get_the_ID(), 'custom_title', true );
$url = get_post_meta( get_the_ID(), 'custom_url', true );
$body = get_post_meta( get_the_ID(), 'custom_content', true );
?>
<h2><?php echo esc_html( $title ); ?></h2>
<a href="<?php echo esc_url( $url ); ?>" title="<?php echo esc_attr( $title ); ?>">
<?php echo esc_html( $title ); ?>
</a>
<div class="content">
<?php echo wp_kses_post( $body ); ?>
</div>
Safe Handling of URLs and Query Parameters
URL parameters are often considered harmless because they are not stored in the database. In reality, they are a common channel for reflected XSS: an attacker creates a specially crafted link, and the site renders the parameter value in a heading, message, or attribute without escaping it.
First, retrieve the value using the expected type, then apply the appropriate function before output. If a parameter must be a number or one of several allowed values, do not accept an arbitrary string. For redirects, also verify allowed hosts and schemes so that dangerous or unexpected addresses cannot be used.
<?php
$query = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';
$item_id = isset( $_GET['item'] ) ? absint( $_GET['item'] ) : 0;
?>
<p>Results for: <?php echo esc_html( $query ); ?></p>
<div data-item-id="<?php echo esc_attr( $item_id ); ?>"></div>
Custom Widgets, Shortcodes, and Elementor Custom Code
In a custom Elementor widget, do not return data through echo without validation. Escape values in the method that generates the final HTML markup. If the widget allows users to enter HTML, clearly define the permitted tags, attributes, and URL protocols.
Shortcodes have the same issue: shortcode attributes can be changed by users, and their values should not be considered trusted. Even if a shortcode is intended for administrators only, protection is required at the code level—roles, imports, or a compromised account may change its data.
The HTML widget and Custom Code effectively allow arbitrary markup or scripts to be inserted. Access to them should be limited to trusted administrators. Do not grant these permissions to authors, editors, or client roles without a clear need and appropriate oversight.
- do not output field values through an unprocessed echo;
- do not use esc_html() for URLs instead of esc_url();
- do not allow arbitrary HTML simply because it is stored in a custom field;
- do not insert user data directly into inline JavaScript;
- check permissions for editing HTML, Custom Code, and templates;
- after changes, check both the published page and the preview.
<?php
function webhva_notice_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'text' => '',
'type' => 'info',
),
$atts,
'webhva_notice'
);
$allowed_types = array( 'info', 'warning', 'success' );
$type = in_array( $atts['type'], $allowed_types, true ) ? $atts['type'] : 'info';
return '<div class="notice notice-' . esc_attr( $type ) . '">' .
esc_html( $atts['text'] ) .
'</div>';
}
Common Mistakes That Create Vulnerabilities
XSS most often appears not in one obvious place, but because of an incorrect assumption: “only an administrator edits this field” or “we already cleaned the value when saving it.” Security must remain effective even after changing the import method, user roles, or template.
Check HTML, attributes, and JavaScript separately. Escaping that is safe in one context may be insufficient in another. For example, an HTML string cannot be transferred blindly into JavaScript, and an href or src value should not be built by concatenating an unvalidated parameter.
- outputting get_post_meta(), get_field(), or $_GET without escaping;
- using esc_attr() for a complete HTML block instead of wp_kses_post();
- allowing raw HTML where ordinary text would be sufficient;
- trusting database data simply because it was once entered by an administrator;
- building href or src from an unvalidated value;
- passing data into data-* attributes without esc_attr();
- inserting a string into script through concatenation without proper JSON encoding.
How to Check an Elementor Site After Changes
Testing should be performed in a staging environment or after creating a backup. Check all fields that authors, users, or visitors can edit, as well as every template where those fields are inserted through dynamic tags.
For manual checks, use safe test markers that show whether input is being converted into HTML rather than text. Do not experiment with real user sessions or insert malicious scripts on a production site.
- search the code for places containing echo, print, innerHTML, and HTML concatenation;
- check custom widgets, shortcodes, and PHP templates;
- review fields rendered as text, attributes, URLs, and scripts;
- check the page as a guest and as an authenticated user;
- make sure the HTML widget and Custom Code are available only to trusted roles;
- update WordPress, Elementor, the theme, and add-ons, and analyze errors after updates;
- where necessary, supplement protection with a Content Security Policy, but do not treat it as a replacement for correct escaping.
Short Pre-Publication Checklist
Before launching a dynamic template, trace each value through the entire chain: source, storage, retrieval, markup generation, and browser rendering. For every point, it should be clear what format is allowed and which function protects that particular context.
- ordinary text is output through esc_html();
- attributes are output through esc_attr();
- links are validated and output through esc_url();
- HTML is restricted with wp_kses_post() or a custom list of permitted tags;
- numbers, identifiers, and allowlisted values undergo type or allowlist validation;
- data from URLs, forms, and comments is not treated as trusted;
- custom widgets and shortcodes do not produce unprocessed output;
- permissions for HTML and Custom Code are restricted;
- after testing, entered tags or scripts are not executed in the browser.
Frequently asked questions
Is sanitizing a field when it is saved sufficient?
No. Sanitization reduces risk when data is accepted or saved, but every output still requires escaping appropriate to its context: esc_html(), esc_attr(), esc_url(), or wp_kses_post().
Are Elementor dynamic tags secure by default?
The entire chain cannot automatically be considered secure. Standard components may process data, but risks often arise in custom widgets, shortcodes, templates, HTML widgets, or code that extends Elementor.
When should wp_kses_post() be used instead of esc_html()?
Use esc_html() when the value should be ordinary text. Use wp_kses_post() only when the functionality genuinely requires limited HTML. Arbitrary unfiltered HTML should not be allowed.
Can XSS occur through a URL parameter?
Yes. This can be a reflected XSS scenario if a URL value is rendered in a heading, message, attribute, or script without processing. Retrieve the parameter using the required type and escape it immediately before output.
Conclusion
Protecting Elementor from XSS starts with controlling the context in which a dynamic value is rendered. Do not trust data simply because it is stored in WordPress or entered through the admin area.
Validate data when receiving and saving it, and escape it at output: text with esc_html(), attributes with esc_attr(), URLs with esc_url(), and permitted HTML with wp_kses_post(). Pay particular attention to custom widgets, shortcodes, the HTML widget, and Custom Code.