Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"plugins": [
"."
]
}
15 changes: 15 additions & 0 deletions src/blockparty-iframe/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@
"url": {
"type": "string",
"default": ""
},
"iframeAttributes": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
}
},
"textdomain": "blockparty-iframe",
Expand Down
59 changes: 42 additions & 17 deletions src/blockparty-iframe/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { useState } from '@wordpress/element';
import './editor.scss';

import { aspectRatio } from '@wordpress/icons';
import { convertAttributesToProps, parseIframeCode } from './utils';

/**
* The edit function describes the structure of your block in the context of the
Expand All @@ -50,12 +51,18 @@ import { aspectRatio } from '@wordpress/icons';
*/
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const { lazyload, title: initialTitle, url: initialUrl } = attributes;
const {
lazyload,
title: initialTitle,
url: initialUrl,
iframeAttributes: initialAttributes,
} = attributes;

// State local pour les champs TextControl
const [ iframeData, setIframeData ] = useState( {
url: initialUrl || '',
title: initialTitle || '',
iframeAttributes: initialAttributes || [],
} );

// hasConfirmed = l’utilisateur a validé l’ajout de l’iframe
Expand All @@ -76,6 +83,29 @@ export default function Edit( { attributes, setAttributes } ) {
const showPlaceholder = ! hasConfirmed;
const showIframe = hasConfirmed && isIframeElligible;

// Handle URL/iframe code change
function handleUrlChange( value ) {
// Try to parse as iframe code
const parsed = parseIframeCode( value );

if ( parsed ) {
// It's an iframe code, extract URL, title, and attributes
setIframeData( {
...iframeData,
url: parsed.url,
title: parsed.title || iframeData.title, // Use extracted title if available, otherwise keep current
iframeAttributes: parsed.attributes,
} );
} else {
// It's a regular URL
setIframeData( {
...iframeData,
url: value,
iframeAttributes: [],
} );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Editing URL clears previously extracted iframe attributes

Medium Severity

When iframe code is pasted, the URL is extracted and shown in the input field while iframeAttributes stores the parsed attributes. If the user then modifies the URL in any way (even a small edit), handleUrlChange no longer recognizes it as iframe code and sets iframeAttributes: [], silently discarding all previously extracted attributes like allowfullscreen or allow. This causes unexpected loss of iframe functionality when users make minor URL corrections.

Fix in Cursor Fix in Web

}

// Handle clic Add iframe
function handleAddIframeButtonClick() {
setAttributes( { ...attributes, ...iframeData } );
Expand Down Expand Up @@ -118,28 +148,20 @@ export default function Edit( { attributes, setAttributes } ) {
icon={ aspectRatio }
label={ __( 'Iframe', 'blockparty-iframe' ) }
instructions={ __(
'Fill the URL and the title of the iframe.',
'Fill the iframe source and the title of the iframe.',
'blockparty-iframe'
) }
>
<div style={ { width: '100%' } }>
<TextControl
label={ __( 'URL', 'blockparty-iframe' ) }
label={ __( 'Source', 'blockparty-iframe' ) }
value={ iframeData.url }
onChange={ ( value ) =>
setIframeData( { ...iframeData, url: value } )
}
placeholder="https://..."
type="url"
help={
iframeData.url.length &&
! isURL( iframeData.url )
? __(
'The URL is invalid.',
'blockparty-iframe'
)
: ''
}
onChange={ handleUrlChange }
placeholder={ `https://... or <iframe src="https://..."` }
help={ __(
'You can either paste a URL or the iframe code.',
'blockparty-iframe'
) }
/>

<TextControl
Expand Down Expand Up @@ -170,6 +192,9 @@ export default function Edit( { attributes, setAttributes } ) {
title={ iframeData.title }
src={ iframeData.url }
loading={ lazyload ? 'lazy' : 'eager' }
{ ...convertAttributesToProps(
iframeData.iframeAttributes
) }
/>
) }
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/blockparty-iframe/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
*/
import { useBlockProps } from '@wordpress/block-editor';
import { convertAttributesToProps } from './utils';

/**
* The save function defines the way in which the different attributes should
Expand All @@ -16,7 +17,7 @@ import { useBlockProps } from '@wordpress/block-editor';
* @return {Element} Element to render.
*/
export default function save( { attributes } ) {
const { lazyload, title, url } = attributes;
const { lazyload, title, url, iframeAttributes } = attributes;

if ( ! url || ! title ) {
return <div { ...useBlockProps.save() } />;
Expand All @@ -28,6 +29,7 @@ export default function save( { attributes } ) {
title={ title }
src={ url }
loading={ lazyload ? 'lazy' : 'eager' }
{ ...convertAttributesToProps( iframeAttributes ) }
/>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/blockparty-iframe/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@
width: 100%;
height: 100%;
}

&:not(.has-aspect-ratio) {
aspect-ratio: 1;
min-height: unset;
}
}
137 changes: 137 additions & 0 deletions src/blockparty-iframe/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Utility functions for iframe attribute handling.
*/

/**
* Map HTML attribute names to React prop names.
*
* @param {string} attributeName - The HTML attribute name.
* @return {string} The React prop name.
*/
export function mapHtmlAttributeToReact( attributeName ) {
const attributeMap = {
allowfullscreen: 'allowFullScreen',
allowpaymentrequest: 'allowPaymentRequest',
referrerpolicy: 'referrerPolicy',
};

return attributeMap[ attributeName.toLowerCase() ] || attributeName;
}

/**
* Check if an attribute is a boolean HTML attribute.
*
* @param {string} attributeName - The name of the attribute to check.
* @return {boolean} True if the attribute is boolean, false otherwise.
*/
export function isBooleanAttribute( attributeName ) {
const booleanAttrs = [ 'allowfullscreen', 'allowpaymentrequest' ];

return booleanAttrs.includes( attributeName.toLowerCase() );
}

/**
* Convert iframe attributes array to props object for React.
* Handles boolean attributes and React prop name mapping correctly.
*
* @param {Array} attributes - Array of {key, value} objects.
* @return {Object} Props object for React component.
*/
export function convertAttributesToProps( attributes ) {
return Object.fromEntries(
( attributes || [] ).map( ( attr ) => {
// Map HTML attribute name to React prop name
const propName = mapHtmlAttributeToReact( attr.key );

// Convert 'true' string to boolean for boolean attributes
const value =
isBooleanAttribute( attr.key ) && attr.value === 'true'
? true
: attr.value;

return [ propName, value ];
} )
);
}

/**
* Check if an iframe attribute should be excluded.
*
* @param {string} attributeName - The name of the attribute to check.
* @return {boolean} True if the attribute should be excluded, false otherwise.
*/
export function isExcludedIframeAttribute( attributeName ) {
const excludedAttrs = [
'src', // Managed separately
'loading', // Managed by lazyload option
'title', // Managed separately
'width', // Managed by block dimension supports
'height', // Managed by block dimension supports
'style', // Requires object format in React, not string
'frameborder', // Deprecated HTML attribute
'marginwidth', // Deprecated HTML attribute
'marginheight', // Deprecated HTML attribute
'scrolling', // Deprecated HTML attribute
'align', // Deprecated HTML attribute
'longdesc', // Deprecated HTML attribute
'name', // Can cause conflicts
];

return excludedAttrs.includes( attributeName.toLowerCase() );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfiltered iframe attributes allow potential XSS attacks

High Severity

The isExcludedIframeAttribute function doesn't filter event handlers (onload, onerror, etc.) or the srcdoc attribute. When pasting iframe code with these attributes, they're extracted by parseIframeCode, stored, and rendered in the saved HTML via convertAttributesToProps. The srcdoc attribute is particularly dangerous as it allows embedding arbitrary HTML/JavaScript content. Malicious iframe code could execute JavaScript when the saved content is viewed.

Additional Locations (1)

Fix in Cursor Fix in Web


/**
* Parse iframe HTML code and extract src URL, title, and attributes.
*
* @param {string} value - The value that could be a URL or iframe HTML code.
* @return {Object|null} Object with url, title, and attributes array, or null if not an iframe.
*/
export function parseIframeCode( value ) {
// Check if the value contains iframe tag
const iframeRegex = /<iframe[^>]*>/i;
const match = value.match( iframeRegex );

if ( ! match ) {
return null;
}

// Extract only the opening tag (ignore any content inside iframe)
const iframeTag = match[ 0 ];

// Create a temporary DOM element to parse the HTML
// Use a self-closing iframe to avoid parsing issues with content
const tempDiv = document.createElement( 'div' );
tempDiv.insertAdjacentHTML( 'beforeend', iframeTag + '</iframe>' );
const iframeElement = tempDiv.querySelector( 'iframe' );

if ( ! iframeElement ) {
return null;
}

// Extract src attribute
const src = iframeElement.getAttribute( 'src' ) || '';

// Extract title attribute
const title = iframeElement.getAttribute( 'title' ) || '';

// Extract all other attributes (excluding managed and deprecated ones)
const attributes = [];

for ( const attr of iframeElement.attributes ) {
if ( ! isExcludedIframeAttribute( attr.name ) ) {
// For boolean attributes, store 'true' as value if present
const value = isBooleanAttribute( attr.name ) ? 'true' : attr.value;

attributes.push( {
key: attr.name,
value,
} );
}
}

return {
url: src,
title,
attributes,
};
}
Loading