Type something to search...
What is cross-site scripting (XSS) and how to prevent it?

What is cross-site scripting (XSS) and how to prevent it?

Cross-site scripting (XSS) is a vulnerability that lets an attacker inject malicious JavaScript into a web page so that it runs in other visitors' browsers, with the same access as the site's own code. You prevent it by treating all user-controlled data as untrusted: escape it correctly for the context where it's output, sanitize any HTML you must allow, avoid dangerous JavaScript APIs, and add a Content Security Policy as a second line of defense.

XSS is one of the most frequently reported vulnerabilities in web applications and WordPress plugins. It's also frequently underestimated, because a harmless-looking alert() box in a proof of concept hides how much damage a real payload can do. This article explains how XSS works, the three main types, what attackers can achieve, and how to write and configure your site so XSS can't take hold, with examples for WordPress, plain PHP, JavaScript, and React.

How Does Cross-Site Scripting Work?

Browsers trust the code a website sends them. When a page includes JavaScript, the browser runs it with full access to that page: its content, its cookies (unless protected), its forms, and anything the logged-in user can do. XSS abuses that trust.

The vulnerability appears when an application takes input from a user, such as a comment, a search term, a profile field, or a URL parameter, and includes it in a page without properly escaping it. If the input contains HTML or JavaScript, the browser can't tell it apart from the site's legitimate code.

Here's a simple vulnerable example in PHP:

// VULNERABLE: never output raw input
echo '<p>You searched for: ' . $_GET['q'] . '</p>';

If someone visits the search page with a query like <script>alert(1)</script>, the browser receives that script as part of the page and runs it. A real attacker would replace the alert with code that does something harmful.

What Can Attackers Do with XSS?

Because the injected script runs as your site, it can do almost anything a legitimate script could:

  • Hijack sessions: Read cookies that aren't marked HttpOnly and send them to the attacker.
  • Act as the user: Submit forms, change settings, or create new admin accounts if an administrator views the infected page.
  • Steal data: Read personal information displayed on the page or keystrokes entered into forms.
  • Phish credentials: Replace the page with a convincing fake login form.
  • Deface content: Change what visitors see.
  • Spread malware or redirects: Send visitors to malicious sites.

On WordPress, stored XSS that fires when an administrator views a page is especially dangerous, because an admin's browser can install plugins or create users, turning an XSS bug into full site takeover.

The Three Main Types of XSS

Stored (Persistent) XSS

The malicious input is saved on the server, for example in a comment, product review, forum post, or user profile field, and shown to everyone who views that content. It's the most dangerous type because it affects every visitor without any special link.

Reflected XSS

The input is included in the immediate response, usually from a URL parameter or form submission, but not stored. The attacker has to trick a victim into clicking a crafted link, often through email or social media. Search pages and error messages are common culprits.

DOM-Based XSS

The vulnerability lives entirely in client-side JavaScript. The page's own script takes data from a source like location.hash or location.search and writes it into the page using a dangerous API. The server may never see the payload.

// VULNERABLE: writes untrusted data as HTML
const name = new URLSearchParams(location.search).get("name");
document.getElementById("greeting").innerHTML = "Hello, " + name;

Using innerHTML, outerHTML, document.write(), or insertAdjacentHTML() with untrusted data is a classic source of DOM-based XSS.

How to Prevent XSS

1. Escape Output for the Right Context

The core rule is to escape data at the moment you output it, using the method that matches where it's going. HTML body text, HTML attributes, URLs, and JavaScript all need different escaping.

In WordPress

WordPress provides context-specific escaping functions. Use them every time you output dynamic data:

FunctionUse it for
esc_html()Text inside HTML elements
esc_attr()Values inside HTML attributes
esc_url()URLs in href, src, and similar attributes
esc_js()Short strings inside inline JavaScript
esc_textarea()Content inside a textarea
wp_kses_post()HTML that should allow safe post-level tags
wp_json_encode()Passing data to JavaScript safely

Here's the search example written safely:

$query = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';

printf(
    '<p>%s <strong>%s</strong></p>',
    esc_html__( 'You searched for:', 'sajjad' ),
    esc_html( $query )
);

And an example with attributes and URLs:

$profile_url = get_user_meta( $user_id, 'sajjad_website', true );
$label       = get_the_author_meta( 'display_name', $user_id );
?>
<a href="<?php echo esc_url( $profile_url ); ?>" title="<?php echo esc_attr( $label ); ?>">
    <?php echo esc_html( $label ); ?>
</a>
<?php

esc_url() also strips dangerous schemes like javascript:, which is a common way to sneak script into links.

In Plain PHP

Use htmlspecialchars() with the right flags and character set:

function sajjad_e( string $value ): string {
    return htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8' );
}

echo '<p>You searched for: ' . sajjad_e( $_GET['q'] ?? '' ) . '</p>';

Most modern template engines, including Twig and Blade, escape output automatically. Be careful with their "raw" output modes.

2. Sanitize HTML You Must Allow

Sometimes you want users to submit limited HTML, such as bold text and links in comments. Escaping would destroy that formatting, so you need to sanitize instead, keeping only an allowlist of safe tags and attributes.

In WordPress, wp_kses() lets you define exactly what's allowed:

$allowed = array(
    'a'      => array(
        'href'  => array(),
        'title' => array(),
        'rel'   => array(),
    ),
    'strong' => array(),
    'em'     => array(),
    'br'     => array(),
);

$clean_bio = wp_kses( wp_unslash( $_POST['bio'] ?? '' ), $allowed );

In JavaScript, use a well-maintained sanitizer like DOMPurify before inserting HTML into the page. Never write your own regex-based HTML sanitizer; they're notoriously easy to bypass.

3. Use Safe JavaScript APIs

In front-end code, prefer APIs that treat data as text rather than HTML:

const name = new URLSearchParams(location.search).get("name") ?? "friend";
document.getElementById("greeting").textContent = `Hello, ${name}`;

textContent, innerText, setAttribute() for safe attributes, and document.createElement() are all safe for untrusted data. Avoid eval(), new Function(), and passing strings to setTimeout() or setInterval().

When passing data from PHP to JavaScript in WordPress, use wp_add_inline_script() with wp_json_encode() rather than printing variables directly into a script tag:

add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script( 'sajjad-app', get_stylesheet_directory_uri() . '/js/app.js', array(), '1.0.0', true );

    $data = array(
        'restUrl' => esc_url_raw( rest_url( 'sajjad/v1/' ) ),
        'nonce'   => wp_create_nonce( 'wp_rest' ),
    );

    wp_add_inline_script(
        'sajjad-app',
        'window.sajjadApp = ' . wp_json_encode( $data ) . ';',
        'before'
    );
} );

4. Be Careful in React and Other Frameworks

React, Vue, Angular, and Svelte escape values in templates automatically, which prevents most XSS. The danger comes from escape hatches:

  • React's dangerouslySetInnerHTML
  • Vue's v-html
  • Angular's bypassSecurityTrustHtml
  • User-controlled URLs in href attributes, which can contain javascript: links

If you must render HTML in React, sanitize it first:

import DOMPurify from "dompurify";

export function Bio({ html }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

And validate URLs before using them in links:

function SafeLink({ href, children }) {
  let safeHref = "#";
  try {
    const url = new URL(href, window.location.origin);
    if (url.protocol === "https:" || url.protocol === "http:") {
      safeHref = url.href;
    }
  } catch {
    // Invalid URL, keep the fallback.
  }
  return (
    <a href={safeHref} rel="noopener noreferrer">
      {children}
    </a>
  );
}

5. Validate and Sanitize Input

Input validation is a supporting defense. Reject input that doesn't match the expected format, and sanitize it before storing. In WordPress, sanitize_text_field(), sanitize_email(), sanitize_key(), and absint() cover most cases. The golden rule is still to escape on output, because data can reach your pages from many paths.

6. Add a Content Security Policy

A Content Security Policy (CSP) is an HTTP header that tells the browser which scripts are allowed to run. A strict CSP can stop injected scripts from executing even if an XSS bug slips through.

A nonce-based policy looks like this:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM_PER_REQUEST'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

Each legitimate script tag carries the matching nonce attribute, and the value changes on every request. Injected scripts without the nonce are blocked. Deploying CSP on a plugin-heavy WordPress site takes testing, so start with the Content-Security-Policy-Report-Only header to see what would break before enforcing it. A separate article on this blog covers CSP in a Next.js app.

7. Protect Cookies

Mark session cookies as HttpOnly so JavaScript can't read them, Secure so they're only sent over HTTPS, and use SameSite=Lax or Strict. WordPress sets its authentication cookies as HttpOnly by default. This doesn't prevent XSS, but it limits what a successful attack can steal.

8. Keep Plugins and Themes Updated

As with most vulnerability classes, XSS on WordPress sites usually comes from third-party code. Stay current with updates, remove plugins you don't use, and consider a WAF that can block common XSS payloads and virtually patch known issues.

9. Limit Who Can Post Unfiltered HTML

In WordPress, Administrators and Editors on single sites have the unfiltered_html capability, which lets them add scripts to posts. Only give those roles to people you trust. You can remove the capability site-wide by adding this to wp-config.php above "That's all, stop editing!":

define( 'DISALLOW_UNFILTERED_HTML', true );

Test your content afterwards, since embeds or custom HTML blocks that relied on scripts may stop working.


FAQ: Cross-Site Scripting (XSS)

Cross-site scripting is when an attacker gets their own JavaScript to run on your website in other people's browsers, usually by submitting it through a form or link that the site displays without proper escaping.

Stored XSS saves the malicious script on the server, such as in a comment, so it runs for every visitor. Reflected XSS bounces the script back immediately from a URL or form, so the attacker has to trick someone into clicking a crafted link.

No. HTTPS encrypts the connection, but if your site sends a page containing injected script, the browser will run it over HTTPS just the same. XSS must be fixed in how your site handles and outputs data.

Not on its own. Input sanitization helps, but data can reach your pages from many sources. The reliable defense is escaping on output for the correct context, supported by input validation and a Content Security Policy.

React escapes values rendered in JSX, which prevents most XSS. You can still introduce XSS through dangerouslySetInnerHTML, unvalidated URLs in links, or direct DOM manipulation, so those need care.

A strict CSP blocks most injected scripts, but a weak policy with broad allowances may not. CSP is best seen as a strong second layer behind proper output escaping.

Use a security plugin or service like Wordfence, Patchstack, or WPScan, which track known plugin vulnerabilities and alert you. Keeping plugins updated fixes most reported XSS issues.


Conclusion

Cross-site scripting lets attackers run their JavaScript inside your pages, where it can steal sessions, act on behalf of users, phish credentials, and on WordPress even take over a site through an administrator's browser. It comes in stored, reflected, and DOM-based forms, but all three stem from treating untrusted data as code.

The fixes are consistent: escape every piece of dynamic output for its context, sanitize any HTML you allow with a proper allowlist, use safe JavaScript APIs and framework defaults, protect cookies, and add a Content Security Policy as a safety net. Keep plugins updated and limit unfiltered HTML to trusted users, and XSS becomes a problem you've designed out of your site rather than one you're waiting to discover.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
How does GDPR affect website security?

How does GDPR affect website security?

GDPR affects website security by turning it from a good habit into a legal obligation. If your website collects personal data from people in the EU (

Dive Deeper