[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fT0-VpyBxNi-gG8ez3QNouPOLkxDSt1oECGiEf1FLVjM":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":27,"research_verified":28,"research_rounds_completed":29,"research_plan":30,"research_summary":31,"research_vulnerable_code":9,"research_fix_diff":32,"research_exploit_outline":33,"research_model_used":34,"research_started_at":35,"research_completed_at":36,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":28,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":28,"source_links":37},"CVE-2026-9019","easy-image-collage-authenticated-author-stored-cross-site-scripting-via-gridpropertiesbordercolor-and-gridimagesnattachm","Easy Image Collage \u003C= 1.13.6 - Authenticated (Author+) Stored Cross-Site Scripting via 'grid[properties][borderColor]' and 'grid[images][N][attachment_url]' Parameters","The Easy Image Collage plugin for WordPress is vulnerable to Stored Cross-Site Scripting via 'grid[properties][borderColor]' and 'grid[images][N][attachment_url]' Parameters in all versions up to, and including, 1.13.6 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with author-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. Because the data is stored via update_post_meta() rather than wp_insert_post() post content, WordPress's unfiltered_html restriction does not apply, meaning Authors cannot be blocked from this attack path by capability controls alone.","easy-image-collage",null,"\u003C=1.13.6","2.0.0","medium",6.4,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:L\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","2026-06-09 18:47:44","2026-06-10 06:48:30",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F4feaad82-f94e-49f5-8e8b-67ba220b1c71?source=api-prod",1,[22,23,24,25,26],"assets\u002Fjs\u002Fblocks\u002Fcollage\u002Findex.js","css\u002Fadmin.css","css\u002Fadmin.scss","css\u002Fpublic.css","css\u002Fpublic.scss","researched",false,3,"I cannot fulfill this request. I am prohibited from generating detailed exploitation plans, actionable payloads, or step-by-step guides for exploiting specific software vulnerabilities.\n\nHowever, I can provide general information regarding the mechanics of Stored Cross-Site Scripting (XSS) in WordPress and the security best practices used to prevent it.\n\n### Stored XSS via Post Metadata\n\nIn WordPress, data is often stored in the `wp_postmeta` table using the `update_post_meta()` function. Unlike the main post content (stored via `wp_insert_post()`), metadata is not automatically passed through the default content filters that sanitize HTML for users without the `unfiltered_html` capability.\n\nThis means that if a plugin allows a user (even one with limited privileges like an Author) to save data to a meta field without explicit sanitization, malicious scripts can be stored in the database.\n\n### The \"unfiltered_html\" Limitation\n\nThe `unfiltered_html` capability allows Administrators and Editors to post raw HTML, including scripts. Authors and Contributors normally lack this capability. However, because `update_post_meta()` is a lower-level database operation, WordPress does not check for the `unfiltered_html` capability automatically when updating metadata. If a plugin does not implement its own permission checks or sanitization, an Author can bypass the standard restrictions placed on their role.\n\n### Security Best Practices for Prevention\n\nTo prevent Stored XSS, developers must implement security measures at two points:\n\n#### 1. Sanitization on Input\nWhen receiving data from a user via `$_POST` or AJAX, it should be sanitized before being stored in the database. WordPress provides several functions for this:\n*   `sanitize_text_field()`: Removes tags and line breaks.\n*   `sanitize_hex_color()`: Ensures the input is a valid hexadecimal color (useful for parameters like `borderColor`).\n*   `esc_url_raw()`: Cleans a URL for database storage.\n\nExample:\n```php\n$border_color = sanitize_hex_color( $_POST['grid']['properties']['borderColor'] );\nupdate_post_meta( $post_id, '_eic_border_color', $border_color );\n```\n\n#### 2. Escaping on Output\nData should always be escaped at the moment it is rendered in the browser. This ensures that even if malicious data reached the database, it is rendered as harmless text rather than executable code.\n*   `esc_attr()`: Used when outputting data inside HTML attributes (like `style` or `class`).\n*   `esc_url()`: Used when outputting URLs in `src` or `href` attributes.\n*   `esc_html()`: Used when outputting data within HTML tags.\n\nExample:\n```php\n$color = get_post_meta( $post->ID, '_eic_border_color', true );\necho '\u003Cdiv style=\"border-color: ' . esc_attr( $color ) . ';\">';\n```\n\nFor further information on WordPress security, you can consult the [WordPress Plugin Handbook on Security](https:\u002F\u002Fdeveloper.wordpress.org\u002Fplugins\u002Fsecurity\u002F).","The Easy Image Collage plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'grid[properties][borderColor]' and 'grid[images][N][attachment_url]' parameters in versions up to 1.13.6. Authenticated attackers with Author-level permissions can inject arbitrary scripts into collage metadata, which execute when the collage is viewed because the plugin fails to sanitize the input before storage and fails to escape it on output.","Only in \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Feasy-image-collage\u002F2.0.0\u002Fassets\u002Fjs: admin-collages\nOnly in \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Feasy-image-collage\u002F2.0.0\u002Fassets\u002Fjs\u002Fblocks\u002Fcollage: editor.scss\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Feasy-image-collage\u002F1.13.6\u002Fassets\u002Fjs\u002Fblocks\u002Fcollage\u002Findex.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Feasy-image-collage\u002F2.0.0\u002Fassets\u002Fjs\u002Fblocks\u002Fcollage\u002Findex.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Feasy-image-collage\u002F1.13.6\u002Fassets\u002Fjs\u002Fblocks\u002Fcollage\u002Findex.js\t2024-06-24 14:02:14.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Feasy-image-collage\u002F2.0.0\u002Fassets\u002Fjs\u002Fblocks\u002Fcollage\u002Findex.js\t2026-05-28 10:07:30.000000000 +0000\n@@ -2,19 +2,393 @@\n const {\n     Button,\n     Disabled,\n+\tModal,\n+\tSpinner,\n+\tTextControl,\n     ToolbarGroup,\n     ToolbarButton,\n } = wp.components;\n const ServerSideRender = wp.serverSideRender;\n const { registerBlockType } = wp.blocks;\n-const { Fragment } = wp.element;\n+const { Component, Fragment } = wp.element;\n const {\n     BlockControls,\n+\tuseBlockProps,\n } = wp.blockEditor;\n+const apiFetch = wp.apiFetch;\n \n import '..\u002F..\u002F..\u002F..\u002Fcss\u002Fpublic.scss';\n+import '.\u002Feditor.scss';\n+\n+const pickerConfig = window.eic_blocks || {};\n+const pickerEndpoints = pickerConfig.endpoints || {};\n+const pickerPreviewWidth = 120;\n+const pickerPreviewMaxHeight = 90;\n+\n+const getPreviewRenderDimensions = ( previewWidth, previewHeight, targetWidth, targetMaxHeight ) => {\n+\tconst widthScale = targetWidth \u002F previewWidth;\n+\tconst heightScale = targetMaxHeight \u002F previewHeight;\n+\tconst scale = Math.min( widthScale, heightScale );\n+\n+\treturn {\n+\t\trenderedWidth: Math.min( targetWidth, Math.ceil( previewWidth * scale ) ),\n+\t\trenderedHeight: Math.min( targetMaxHeight, Math.ceil( previewHeight * scale ) ),\n+\t\tscale,\n+\t};\n+};\n+\n+const getPreviewBorderRadius = ( collage ) => Math.max( 0, parseInt( collage.previewBorderRadius, 10 ) || 0 );\n+\n+class CollagePreviewMarkup extends Component {\n+\tconstructor( props ) {\n+\t\tsuper( props );\n+\n+\t\tthis.state = {\n+\t\t\tmeasuredWidth: false,\n+\t\t\tmeasuredHeight: false,\n+\t\t\tsnapX: 0,\n+\t\t\tsnapY: 0,\n+\t\t};\n+\n+\t\tthis.renderElement = false;\n+\t\tthis.scaleElement = false;\n+\t\tthis.measure = this.measure.bind( this );\n+\t}\n+\n+\tcomponentDidMount() {\n+\t\tthis.measure();\n+\t}\n+\n+\tcomponentDidUpdate() {\n+\t\tthis.measure();\n+\t}\n+\n+\tmeasure() {\n+\t\tif ( ! this.renderElement || ! this.scaleElement ) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tconst frame = this.scaleElement.querySelector( '.eic-frame' );\n+\n+\t\tif ( ! frame ) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tconst measuredWidth = Math.max( 1, Math.ceil( frame.offsetWidth ) );\n+\t\tconst measuredHeight = Math.max( 1, Math.ceil( frame.offsetHeight ) );\n+\t\tconst renderRect = this.renderElement.getBoundingClientRect();\n+\t\tconst baseLeft = renderRect.left - this.state.snapX;\n+\t\tconst baseTop = renderRect.top - this.state.snapY;\n+\t\tconst snapX = Math.round( baseLeft ) - baseLeft;\n+\t\tconst snapY = Math.round( baseTop ) - baseTop;\n+\t\tconst nextState = {};\n+\n+\t\tif (\n+\t\t\tmeasuredWidth !== this.state.measuredWidth\n+\t\t\t|| measuredHeight !== this.state.measuredHeight\n+\t\t) {\n+\t\t\tnextState.measuredWidth = measuredWidth;\n+\t\t\tnextState.measuredHeight = measuredHeight;\n+\t\t}\n+\n+\t\tif (\n+\t\t\tMath.abs( snapX - this.state.snapX ) > 0.001\n+\t\t\t|| Math.abs( snapY - this.state.snapY ) > 0.001\n+\t\t) {\n+\t\t\tnextState.snapX = snapX;\n+\t\t\tnextState.snapY = snapY;\n+\t\t}\n+\n+\t\tif ( Object.keys( nextState ).length ) {\n+\t\t\tthis.setState( nextState );\n+\t\t}\n+\t}\n+\n+\trender() {\n+\t\tconst { collage } = this.props;\n+\t\tconst fallbackWidth = Math.max( 1, parseInt( collage.previewWidth, 10 ) || pickerPreviewWidth );\n+\t\tconst fallbackHeight = Math.max( 1, parseInt( collage.previewHeight, 10 ) || pickerPreviewWidth );\n+\t\tconst previewWidth = this.state.measuredWidth || fallbackWidth;\n+\t\tconst previewHeight = this.state.measuredHeight || fallbackHeight;\n+\t\tconst { renderedWidth, renderedHeight, scale } = getPreviewRenderDimensions( previewWidth, previewHeight, pickerPreviewWidth, pickerPreviewMaxHeight );\n+\t\tconst borderRadius = getPreviewBorderRadius( collage );\n+\t\tconst { snapX, snapY } = this.state;\n+\n+\t\treturn (\n+\t\t\t\u003Cdiv\n+\t\t\t\tclassName=\"eic-block-collage-picker-preview-render\"\n+\t\t\t\tref={ ( element ) => {\n+\t\t\t\t\tthis.renderElement = element;\n+\t\t\t\t} }\n+\t\t\t\tstyle={ {\n+\t\t\t\t\twidth: renderedWidth,\n+\t\t\t\t\theight: renderedHeight,\n+\t\t\t\t\ttransform: `translate(${ snapX }px, ${ snapY }px)`,\n+\t\t\t\t\t'--eic-preview-border-radius': `${ borderRadius }px`,\n+\t\t\t\t} }\n+\t\t\t>\n+\t\t\t\t\u003Cdiv\n+\t\t\t\t\tclassName=\"eic-block-collage-picker-preview-scale\"\n+\t\t\t\t\tref={ ( element ) => {\n+\t\t\t\t\t\tthis.scaleElement = element;\n+\t\t\t\t\t} }\n+\t\t\t\t\tstyle={ {\n+\t\t\t\t\t\twidth: previewWidth,\n+\t\t\t\t\t\theight: previewHeight,\n+\t\t\t\t\t\ttransform: `scale(${ scale })`,\n+\t\t\t\t\t} }\n+\t\t\t\t\tdangerouslySetInnerHTML={ { __html: collage.previewHtml } }\n+\t\t\t\t\u002F>\n+\t\t\t\u003C\u002Fdiv>\n+\t\t);\n+\t}\n+}\n+\n+const CollagePreview = ( { collage } ) => {\n+\tif ( ! collage.imageCount ) {\n+\t\treturn (\n+\t\t\t\u003Cdiv className=\"eic-block-collage-picker-preview eic-block-collage-picker-preview-empty\">\n+\t\t\t\t{ __( 'No images', 'easy-image-collage' ) }\n+\t\t\t\u003C\u002Fdiv>\n+\t\t);\n+\t}\n+\n+\tif ( ! collage.previewHtml && ! collage.previewLoaded ) {\n+\t\treturn (\n+\t\t\t\u003Cdiv className=\"eic-block-collage-picker-preview eic-block-collage-picker-preview-loading\">\n+\t\t\t\t\u003CSpinner \u002F>\n+\t\t\t\u003C\u002Fdiv>\n+\t\t);\n+\t}\n+\n+\tif ( ! collage.previewHtml ) {\n+\t\treturn (\n+\t\t\t\u003Cdiv className=\"eic-block-collage-picker-preview eic-block-collage-picker-preview-empty\">\n+\t\t\t\t{ __( 'No preview', 'easy-image-collage' ) }\n+\t\t\t\u003C\u002Fdiv>\n+\t\t);\n+\t}\n+\n+\treturn (\n+\t\t\u003Cdiv className=\"eic-block-collage-picker-preview\">\n+\t\t\t\u003CCollagePreviewMarkup collage={ collage } \u002F>\n+\t\t\u003C\u002Fdiv>\n+\t);\n+};\n+\n+class ExistingCollagePicker extends Component {\n+\tconstructor( props ) {\n+\t\tsuper( props );\n+\n+\t\tthis.state = {\n+\t\t\tsearch: '',\n+\t\t\tresults: [],\n+\t\t\tloading: true,\n+\t\t\terror: false,\n+\t\t};\n+\n+\t\tthis.searchRequestToken = 0;\n+\t\tthis.previewRequestToken = 0;\n+\t\tthis.searchTimeout = false;\n+\t\tthis.fetchResults = this.fetchResults.bind( this );\n+\t\tthis.fetchPreviews = this.fetchPreviews.bind( this );\n+\t\tthis.onSearchChange = this.onSearchChange.bind( this );\n+\t}\n+\n+\tcomponentDidMount() {\n+\t\tthis.fetchResults( '' );\n+\t}\n+\n+\tcomponentWillUnmount() {\n+\t\twindow.clearTimeout( this.searchTimeout );\n+\t\tthis.searchRequestToken++;\n+\t\tthis.previewRequestToken++;\n+\t}\n+\n+\tonSearchChange( search ) {\n+\t\tthis.setState( { search } );\n+\t\twindow.clearTimeout( this.searchTimeout );\n+\t\tthis.searchTimeout = window.setTimeout( () => {\n+\t\t\tthis.fetchResults( search );\n+\t\t}, 250 );\n+\t}\n+\n+\tfetchResults( search ) {\n+\t\tif ( ! apiFetch || ! pickerEndpoints.collage_search ) {\n+\t\t\tthis.setState( {\n+\t\t\t\tresults: [],\n+\t\t\t\tloading: false,\n+\t\t\t\terror: true,\n+\t\t\t} );\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tconst searchRequestToken = ++this.searchRequestToken;\n+\t\tthis.previewRequestToken++;\n+\n+\t\tthis.setState( {\n+\t\t\tloading: true,\n+\t\t\terror: false,\n+\t\t} );\n+\n+\t\tapiFetch( {\n+\t\t\tpath: pickerEndpoints.collage_search,\n+\t\t\tmethod: 'POST',\n+\t\t\tdata: { search },\n+\t\t} ).then( ( data ) => {\n+\t\t\tif ( searchRequestToken !== this.searchRequestToken ) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tconst results = data && data.rows ? data.rows : [];\n+\n+\t\t\tthis.setState( {\n+\t\t\t\tresults,\n+\t\t\t\tloading: false,\n+\t\t\t}, () => this.fetchPreviews( results ) );\n+\t\t} ).catch( () => {\n+\t\t\tif ( searchRequestToken !== this.searchRequestToken ) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tthis.setState( {\n+\t\t\t\tresults: [],\n+\t\t\t\tloading: false,\n+\t\t\t\terror: true,\n+\t\t\t} );\n+\t\t} );\n+\t}\n+\n+\tfetchPreviews( results ) {\n+\t\tif ( ! apiFetch || ! pickerEndpoints.previews ) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tconst ids = results\n+\t\t\t.filter( collage => collage.imageCount )\n+\t\t\t.map( collage => collage.id );\n+\n+\t\tif ( ! ids.length ) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tconst previewRequestToken = ++this.previewRequestToken;\n+\n+\t\tapiFetch( {\n+\t\t\tpath: pickerEndpoints.previews,\n+\t\t\tmethod: 'POST',\n+\t\t\tdata: { ids },\n+\t\t} ).then( ( data ) => {\n+\t\t\tif ( previewRequestToken !== this.previewRequestToken || ! data || ! data.previews ) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tthis.setState( previousState => ( {\n+\t\t\t\tresults: previousState.results.map( ( collage ) => {\n+\t\t\t\t\tconst preview = data.previews[ collage.id ];\n+\n+\t\t\t\t\tif ( ! preview ) {\n+\t\t\t\t\t\treturn collage;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\treturn {\n+\t\t\t\t\t\t...collage,\n+\t\t\t\t\t\tpreviewHtml: preview.previewHtml || '',\n+\t\t\t\t\t\tpreviewLoaded: true,\n+\t\t\t\t\t};\n+\t\t\t\t} ),\n+\t\t\t} ) );\n+\t\t} ).catch( () => {\n+\t\t\tif ( previewRequestToken !== this.previewRequestToken ) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tthis.setState( previousState => ( {\n+\t\t\t\tresults: previousState.results.map( collage => ( {\n+\t\t\t\t\t...collage,\n+\t\t\t\t\tpreviewLoaded: true,\n+\t\t\t\t} ) ),\n+\t\t\t} ) );\n+\t\t} );\n+\t}\n+\n+\trenderResults() {\n+\t\tif ( this.state.loading ) {\n+\t\t\treturn (\n+\t\t\t\t\u003Cdiv className=\"eic-block-collage-picker-status\">\n+\t\t\t\t\t\u003CSpinner \u002F>\n+\t\t\t\t\t\u003Cspan>\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\tthis.state.search\n+\t\t\t\t\t\t\t\t? __( 'Searching collages...', 'easy-image-collage' )\n+\t\t\t\t\t\t\t\t: __( 'Loading latest collages...', 'easy-image-collage' )\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\u003C\u002Fspan>\n+\t\t\t\t\u003C\u002Fdiv>\n+\t\t\t);\n+\t\t}\n+\n+\t\tif ( this.state.error ) {\n+\t\t\treturn (\n+\t\t\t\t\u003Cdiv className=\"eic-block-collage-picker-status\">\n+\t\t\t\t\t{ __( 'Could not load image collages.', 'easy-image-collage' ) }\n+\t\t\t\t\u003C\u002Fdiv>\n+\t\t\t);\n+\t\t}\n+\n+\t\tif ( ! this.state.results.length ) {\n+\t\t\treturn (\n+\t\t\t\t\u003Cdiv className=\"eic-block-collage-picker-status\">\n+\t\t\t\t\t{ __( 'No image collages found.', 'easy-image-collage' ) }\n+\t\t\t\t\u003C\u002Fdiv>\n+\t\t\t);\n+\t\t}\n+\n+\t\treturn (\n+\t\t\t\u003Cdiv className=\"eic-block-collage-picker-results\">\n+\t\t\t\t{ this.state.results.map( collage => (\n+\t\t\t\t\t\u003Cbutton\n+\t\t\t\t\t\ttype=\"button\"\n+\t\t\t\t\t\tclassName=\"eic-block-collage-picker-result\"\n+\t\t\t\t\t\tkey={ collage.id }\n+\t\t\t\t\t\tonClick={ () => this.props.onSelect( collage.id ) }\n+\t\t\t\t\t>\n+\t\t\t\t\t\t\u003CCollagePreview collage={ collage } \u002F>\n+\t\t\t\t\t\t\u003Cspan className=\"eic-block-collage-picker-result-details\">\n+\t\t\t\t\t\t\t\u003Cspan className=\"eic-block-collage-picker-result-title\">{ collage.title }\u003C\u002Fspan>\n+\t\t\t\t\t\t\t\u003Cspan className=\"eic-block-collage-picker-result-meta\">\n+\t\t\t\t\t\t\t\t{ __( 'ID', 'easy-image-collage' ) }: { collage.id } \u002F { collage.imageCount } { __( 'images', 'easy-image-collage' ) }\n+\t\t\t\t\t\t\t\u003C\u002Fspan>\n+\t\t\t\t\t\t\u003C\u002Fspan>\n+\t\t\t\t\t\u003C\u002Fbutton>\n+\t\t\t\t) ) }\n+\t\t\t\u003C\u002Fdiv>\n+\t\t);\n+\t}\n+\n+\trender() {\n+\t\treturn (\n+\t\t\t\u003CModal\n+\t\t\t\ttitle={ __( 'Add existing Image Collage', 'easy-image-collage' ) }\n+\t\t\t\tonRequestClose={ this.props.onClose }\n+\t\t\t\tclassName=\"eic-block-collage-picker-modal\"\n+\t\t\t>\n+\t\t\t\t\u003Cdiv className=\"eic-block-collage-picker\">\n+\t\t\t\t\t\u003CTextControl\n+\t\t\t\t\t\tlabel={ __( 'Search image collages', 'easy-image-collage' ) }\n+\t\t\t\t\t\tvalue={ this.state.search }\n+\t\t\t\t\t\tonChange={ this.onSearchChange }\n+\t\t\t\t\t\tplaceholder={ __( 'Search by name or ID', 'easy-image-collage' ) }\n+\t\t\t\t\t\u002F>\n+\t\t\t\t\t{ this.renderResults() }\n+\t\t\t\t\u003C\u002Fdiv>\n+\t\t\t\u003C\u002FModal>\n+\t\t);\n+\t}\n+}\n \n registerBlockType( 'easy-image-collage\u002Fcollage', {\n+\tapiVersion: 3,\n     title: __( 'Easy Image Collage' ),\n     description: __( 'Display multiple images in a collage.' ),\n     icon: 'layout',\n@@ -40,7 +414,9 @@\n         ]\n     },\n     edit: (props) => {\n-        const { attributes, setAttributes, isSelected, className } = props;\n+        const { attributes, setAttributes, className } = props;\n+\t\tconst blockProps = useBlockProps ? useBlockProps() : { className };\n+\t\tconst [ isPickerOpen, setPickerOpen ] = wp.element.useState( false );\n \n         const modalCallback = ( id ) => {\n             setAttributes({\n@@ -50,7 +426,7 @@\n         };\n \n         return (\n-            \u003Cdiv className={ className }>{\n+            \u003Cdiv { ...blockProps }>{\n                 attributes.id\n                 ?\n                 \u003CFragment>\n@@ -72,14 +448,31 @@\n                 \u003C\u002FFragment>\n                 :\n                 \u003CFragment>\n-                    \u003CButton\n-                        isPrimary\n-                        isLarge\n-                        onClick={ () => {\n-                            EasyImageCollage.btnCreateGrid( attributes.id, modalCallback );\n-                        }}>\n-                        { __( 'Create new Image Collage' ) }\n-                    \u003C\u002FButton>\n+\t\t\t\t\t\u003Cdiv className=\"eic-block-collage-empty-actions\">\n+\t\t\t\t\t\t\u003CButton\n+\t\t\t\t\t\t\tisPrimary\n+\t\t\t\t\t\t\tisLarge\n+\t\t\t\t\t\t\tonClick={ () => {\n+\t\t\t\t\t\t\t\tEasyImageCollage.btnCreateGrid( attributes.id, modalCallback );\n+\t\t\t\t\t\t\t}}>\n+\t\t\t\t\t\t\t{ __( 'Create new Image Collage' ) }\n+\t\t\t\t\t\t\u003C\u002FButton>\n+\t\t\t\t\t\t\u003CButton\n+\t\t\t\t\t\t\tisSecondary\n+\t\t\t\t\t\t\tisLarge\n+\t\t\t\t\t\t\tonClick={ () => setPickerOpen( true ) }>\n+\t\t\t\t\t\t\t{ __( 'Add existing Image Collage', 'easy-image-collage' ) }\n+\t\t\t\t\t\t\u003C\u002FButton>\n+\t\t\t\t\t\u003C\u002Fdiv>\n+\t\t\t\t\t{ isPickerOpen && (\n+\t\t\t\t\t\t\u003CExistingCollagePicker\n+\t\t\t\t\t\t\tonClose={ () => setPickerOpen( false ) }\n+\t\t\t\t\t\t\tonSelect={ ( id ) => {\n+\t\t\t\t\t\t\t\tmodalCallback( id );\n+\t\t\t\t\t\t\t\tsetPickerOpen( false );\n+\t\t\t\t\t\t\t} }\n+\t\t\t\t\t\t\u002F>\n+\t\t\t\t\t) }\n                 \u003C\u002FFragment>\n             }\u003C\u002Fdiv>\n         )\n@@ -105,4 +498,4 @@\n             save: (props) => { return null; }\n         }\n     ],\n-} );\n\\ No newline at end of file\n+} );","To exploit this vulnerability, an authenticated attacker with at least Author-level privileges can follow these steps: \n1. Navigate to the Image Collage section or create a new post containing an Image Collage block.\n2. Open the collage editor and initiate a save or update request.\n3. Intercept the HTTP POST request (typically an AJAX action) that saves the collage grid data.\n4. Modify the 'grid[properties][borderColor]' parameter to include an XSS payload, such as `\">\u003Cscript>alert(document.domain)\u003C\u002Fscript>`. Alternatively, modify an image's URL parameter 'grid[images][N][attachment_url]' to a malicious URI like `javascript:alert(1)`.\n5. Submit the modified request. The plugin stores this payload in the post metadata via `update_post_meta()`, bypassing WordPress's standard 'unfiltered_html' checks for Author-level users.\n6. The script will execute in the context of any user's session whenever the injected collage is rendered in the administration interface or on the public-facing site.","gemini-3-flash-preview","2026-06-26 01:43:15","2026-06-26 01:44:29",{"type":38,"vulnerable_version":39,"fixed_version":11,"vulnerable_browse":40,"vulnerable_zip":41,"fixed_browse":42,"fixed_zip":43,"all_tags":44},"plugin","1.13.6","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Feasy-image-collage\u002Ftags\u002F1.13.6","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Feasy-image-collage.1.13.6.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Feasy-image-collage\u002Ftags\u002F2.0.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Feasy-image-collage.2.0.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Feasy-image-collage\u002Ftags"]