CVE-2026-2300

BJ Lazy Load <= 1.0.9 - Authenticated (Contributor+) Stored Cross-Site Scripting via Custom HTML Block

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
Unpatched
Patched in
N/A
Time to patch

Description

The BJ Lazy Load plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the `filter_images()` function in all versions up to, and including, 1.0.9. This is due to the use of regex-based HTML processing (`preg_replace`) that does not properly handle HTML attribute boundaries when replacing `src` attributes, allowing crafted content inside a `class` attribute value to be promoted to real DOM attributes after processing. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.0.9
PublishedMay 11, 2026
Last updatedMay 12, 2026
Affected pluginbj-lazy-load
Research Plan
Unverified

This research plan outlines the process for exploiting **CVE-2026-2300**, a Stored Cross-Site Scripting (XSS) vulnerability in the **BJ Lazy Load** plugin for WordPress. --- ### 1. Vulnerability Summary * **Vulnerability:** Authenticated Stored XSS * **Location:** `bj-lazy-load.php` (inferred)…

Show full research plan

This research plan outlines the process for exploiting CVE-2026-2300, a Stored Cross-Site Scripting (XSS) vulnerability in the BJ Lazy Load plugin for WordPress.


1. Vulnerability Summary

  • Vulnerability: Authenticated Stored XSS
  • Location: bj-lazy-load.php (inferred), specifically within the filter_images() and attribute replacement logic.
  • Cause: The plugin uses a preg_replace (or similar regex-based) approach to swap the src attribute of <img> tags with a placeholder and a data-lazy-src attribute. The regex fails to ensure that the src= string it is replacing is actually the src attribute of the HTML tag, rather than a string inside another attribute (like class or alt).
  • Impact: A Contributor+ user can craft an image tag where a malicious payload is placed inside the class attribute. When the plugin processes the tag, it "breaks out" the payload from the class attribute and promotes it to a top-level DOM attribute (e.g., onerror).

2. Attack Vector Analysis

  • Endpoint: WordPress Post/Page Editor (Classic or Gutenberg).
  • Vulnerable Action: Saving or Previewing a post containing a crafted <img> tag.
  • Authenticated Level: Contributor or higher (any role capable of creating/editing posts and using HTML).
  • Preconditions: The plugin must be active, and the "Lazy load images" setting must be enabled (usually default).

3. Code Flow (Inferred)

  1. Entry Point: A user views a post. WordPress triggers the_content filter.
  2. Hook: The plugin registers add_filter( 'the_content', array( $this, 'filter_images' ), 200 );.
  3. Vulnerable Function: filter_images($content) searches for <img> tags using regex.
  4. Attribute Processing: For each match, it attempts to modify the src attribute. A common vulnerable implementation in this plugin version is:
    // Inferred logic
    $html = preg_replace( '/\bsrc=[^ >]*/i', 'src="' . $placeholder . '" data-lazy-src="' . $src . '"', $html );
    
  5. The Sink: If the regex matches a src= string inside class="src='...' ", it replaces the internal part of the class string. The resulting HTML contains the injected payload as a standalone attribute outside of the original quotes.

4. Nonce Acquisition Strategy

Since the goal is to store the XSS payload as a Contributor, we need to bypass or satisfy WordPress's post-creation security.

  • Mechanism: The agent will log in as a Contributor and use the WordPress REST API or the standard post.php handler.
  • Action:
    1. Log in as a Contributor.
    2. Navigate to wp-admin/post-new.php.
    3. Extract the _wpnonce from the page source or use browser_eval to fetch the wp.apiFetch nonces.
    4. JS Variable: window.wpApiSettings.nonce.

5. Exploitation Strategy

The goal is to create a post containing an image tag that tricks the regex.

Payload Construction:
We need to provide a class attribute that contains a "fake" src string followed by our XSS payload.

  • Payload: <img class="src='fake' onerror=alert(document.domain) " src="https://example.com/valid-image.jpg">

Step-by-Step Execution:

  1. Authentication: Log into the WordPress instance as a user with the Contributor role.
  2. Nonce Extraction:
    • Navigate to wp-admin/post-new.php.
    • Execute browser_eval("wp.apiSettings.nonce") to get the REST API nonce.
  3. Post Creation (Payload Injection):
    • Use the http_request tool to send a POST request to /wp-json/wp/v2/posts.
    • Headers: Content-Type: application/json, X-WP-Nonce: [NONCE].
    • Body:
      {
        "title": "Lazy Load XSS Test",
        "content": "<img class=\"src='x' onerror=alert(1) \" src='https://example.com/image.jpg'>",
        "status": "publish"
      }
      
      (Note: Contributors can usually "publish" to "pending", which is sufficient to trigger the XSS in preview or when an admin reviews the post).
  4. Triggering the XSS:
    • Identify the URL of the newly created post (from the API response).
    • Navigate to the post URL using browser_navigate.
    • Wait for the alert(1) to trigger.

6. Test Data Setup

  • Plugin: Install and activate bj-lazy-load version 1.0.9.
  • User: Create a user attacker with the contributor role.
  • Settings: Ensure Lazy Loading is active (checked in wp-admin/options-general.php?page=bj-lazy-load).

7. Expected Results

When the plugin processes the payload:

  • Original: <img class="src='x' onerror=alert(1) " src='https://example.com/image.jpg'>
  • Processed (Inferred): The plugin finds the first src= (inside the class) and replaces it.
  • Resulting HTML: <img class="src="[PLACEHOLDER_URL]" data-lazy-src="x" onerror=alert(1) " src='https://example.com/image.jpg'>
  • Browser Interpretation: The browser sees onerror=alert(1) as a valid attribute because the preceding " was closed prematurely or ignored by the regex replacement logic.

8. Verification Steps

  1. Post-Exploit Check: Use WP-CLI to verify the post content is stored:
    wp post get [POST_ID] --field=content
  2. HTML Inspection: Use the http_request tool to fetch the post's frontend HTML and search for the processed onerror attribute:
    grep "onerror=alert" output.html

9. Alternative Approaches

  • Attribute Variations: If class is sanitized by WordPress before the plugin runs, try using the alt attribute or data-custom attributes:
    <img alt="src='x' onerror=alert(1) " src="...">
  • Classic Editor: If Gutenberg (Block Editor) blocks the specific HTML, use the admin-ajax.php endpoint with the save-post action to send raw HTML, simulating the Classic Editor.
  • Iframe Payload: The plugin also filters <iframe>. Try:
    <iframe class="src='x' onload=alert(1) " src="..."></iframe>
Research Findings
Static analysis — not yet PoC-verified

Summary

The BJ Lazy Load plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) due to an insecure regular expression used to process image and iframe tags for lazy loading. Authenticated attackers with Contributor-level access or higher can inject malicious JavaScript by crafting attributes that trick the plugin's regex into promoting a payload into a functional HTML attribute (like 'onerror').

Vulnerable Code

/* bj-lazy-load.php around line 200 */

public function filter_images( $content ) {
    // ... logic to find tags ...
    
    // Inferred vulnerable implementation: the regex replaces the first occurrence of 'src=' 
    // without verifying it is a top-level attribute of the HTML tag.
    $content = preg_replace( '/\bsrc=([^ >]*)/i', 'src="' . $placeholder . '" data-lazy-src="$1"', $content );

    return $content;
}

Security Fix

--- bj-lazy-load.php
+++ bj-lazy-load.php
@@ -10,6 +10,18 @@
-    $content = preg_replace( '/\bsrc=([^ >]*)/i', 'src="' . $placeholder . '" data-lazy-src="$1"', $content );
+    $content = preg_replace_callback( '/<(img|iframe)\b([^>]*?)>/i', function( $matches ) use ( $placeholder ) {
+        $tag = $matches[0];
+        $attributes = $matches[2];
+
+        // Ensure we only match the actual src attribute of the tag
+        if ( preg_match( '/\bsrc=([\'"])(.*?)\1/i', $attributes, $src_match ) ) {
+            $src_url = $src_match[2];
+            // Remove the original src attribute and prepend the lazy-load attributes
+            $new_attributes = preg_replace( '/\bsrc=([\'"])(.*?)\1/i', '', $attributes );
+            return '<' . $matches[1] . ' src="' . esc_url( $placeholder ) . '" data-lazy-src="' . esc_url( $src_url ) . '"' . $new_attributes . '>';
+        }
+
+        return $tag;
+    }, $content );

Exploit Outline

1. Authentication: The attacker logs into the WordPress dashboard with at least 'Contributor' permissions. 2. Payload Creation: The attacker creates a new post or page and inserts a custom HTML block or uses the Classic Editor's text mode. 3. Injection: The attacker inserts a crafted <img> tag where the 'class' attribute contains a mock 'src' string and an XSS payload: `<img class="src='x' onerror=alert(document.domain) " src="https://example.com/image.jpg">`. 4. Processing: When the plugin processes the post content via 'the_content' filter, its regex identifies the `src='x'` inside the class attribute as the primary image source. 5. Execution: The plugin replaces the internal class content, resulting in broken HTML attributes where the `onerror` handler is promoted to a functional DOM attribute. When an administrator or visitor views the post, the script executes.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.