From beb2ae816d16a727475ce8c1f325bb59026f3ec8 Mon Sep 17 00:00:00 2001 From: mricoul Date: Thu, 2 Jul 2026 17:01:58 +0200 Subject: [PATCH 1/4] feat(languages): add missing translations --- ...r_FR-0e3cdd73079dd5ef56b82e4a974c848c.json | 2 +- languages/blockparty-iframe-fr_FR.mo | Bin 1692 -> 1814 bytes languages/blockparty-iframe-fr_FR.po | 10 +++++++--- languages/blockparty-iframe.pot | 8 ++++++-- src/blockparty-iframe/block.json | 5 +---- src/blockparty-iframe/edit.js | 5 ++++- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/languages/blockparty-iframe-fr_FR-0e3cdd73079dd5ef56b82e4a974c848c.json b/languages/blockparty-iframe-fr_FR-0e3cdd73079dd5ef56b82e4a974c848c.json index eb610b0..c26caa7 100644 --- a/languages/blockparty-iframe-fr_FR-0e3cdd73079dd5ef56b82e4a974c848c.json +++ b/languages/blockparty-iframe-fr_FR-0e3cdd73079dd5ef56b82e4a974c848c.json @@ -1 +1 @@ -{"translation-revision-date":"2026-01-12 11:32+0100","generator":"WP-CLI\/2.11.0","source":"build\/blockparty-iframe\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n != 1);"},"Replace":["Remplacer"],"View":["Voir"],"Settings":["R\u00e9glages"],"Enable lazyloading":["Activer le chargement diff\u00e9r\u00e9"],"Iframe":["Cadre"],"Fill the iframe source and the title of the iframe.":["Veuillez remplir la source du cadre ainsi que son titre."],"Source":["Source"],"You can either paste a URL or the iframe code.":["Vous pouvez soit renseigner une URL soit le code HTML du cadre."],"Title":["Titre"],"The title of the iframe is used for accessibility purposes and it will only be visible to screen readers.":["Le titre du cadre est utilis\u00e9 \u00e0 des fins d\u2019accessibilit\u00e9 et ne sera visible que par les technologies d\u2019assistance."],"Add iframe":["Ajouter un cadre"]}}} \ No newline at end of file +{"translation-revision-date":"2026-07-02 17:01+0200","generator":"WP-CLI\/2.11.0","source":"build\/blockparty-iframe\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n != 1);"},"Replace":["Remplacer"],"View":["Voir"],"Settings":["R\u00e9glages"],"Enable lazyloading":["Activer le chargement diff\u00e9r\u00e9"],"Iframe":["Cadre"],"Fill the iframe source and the title of the iframe.":["Veuillez remplir la source du cadre ainsi que son titre."],"Source":["Source"],"https:\/\/\u2026 or ', esc_attr( $title ), - esc_url( $url ), + $safe_url, esc_attr( $loading ), $extra_attrs ); @@ -59,23 +86,23 @@ private static function build_iframe_attribute_string( array $attributes ): stri continue; } - $key = (string) $attribute['key']; + $key = strtolower( (string) $attribute['key'] ); $value = isset( $attribute['value'] ) ? (string) $attribute['value'] : ''; - if ( self::is_excluded_iframe_attribute( $key ) ) { + if ( ! self::is_allowed_iframe_attribute( $key ) ) { continue; } if ( self::is_boolean_attribute( $key ) ) { if ( 'true' === $value || '' === $value ) { - $html .= ' ' . esc_attr( strtolower( $key ) ); + $html .= ' ' . $key; } continue; } $html .= sprintf( ' %1$s="%2$s"', - esc_attr( $key ), + $key, esc_attr( $value ) ); } @@ -100,29 +127,24 @@ private static function is_boolean_attribute( string $attribute_name ): bool { } /** - * Check if an iframe attribute should be excluded from custom output. + * Check if an iframe attribute can be output. * - * @param string $attribute_name Attribute name. + * The name must be a plain lowercase attribute name (no whitespace, quotes + * or `=`), and either be allowlisted or be an `aria-*` / `data-*` attribute. + * + * @param string $attribute_name Lowercased attribute name. * * @return bool */ - private static function is_excluded_iframe_attribute( string $attribute_name ): bool { - $excluded_attrs = [ - 'src', - 'loading', - 'title', - 'width', - 'height', - 'style', - 'frameborder', - 'marginwidth', - 'marginheight', - 'scrolling', - 'align', - 'longdesc', - 'name', - ]; + private static function is_allowed_iframe_attribute( string $attribute_name ): bool { + if ( 1 !== preg_match( '/^[a-z][a-z0-9-]*$/', $attribute_name ) ) { + return false; + } + + if ( in_array( $attribute_name, self::ALLOWED_ATTRIBUTES, true ) ) { + return true; + } - return in_array( strtolower( $attribute_name ), $excluded_attrs, true ); + return 1 === preg_match( '/^(aria|data)-[a-z0-9-]+$/', $attribute_name ); } } diff --git a/src/blockparty-iframe/edit.js b/src/blockparty-iframe/edit.js index a8ee13c..fa48063 100644 --- a/src/blockparty-iframe/edit.js +++ b/src/blockparty-iframe/edit.js @@ -26,7 +26,6 @@ import { ToolbarGroup, ToolbarButton, } from '@wordpress/components'; -import { isURL } from '@wordpress/url'; import { useState } from '@wordpress/element'; @@ -39,7 +38,11 @@ import { useState } from '@wordpress/element'; import './editor.scss'; import { aspectRatio } from '@wordpress/icons'; -import { convertAttributesToProps, parseIframeCode } from './utils'; +import { + convertAttributesToProps, + isSafeIframeUrl, + parseIframeCode, +} from './utils'; /** * The edit function describes the structure of your block in the context of the @@ -71,7 +74,7 @@ export default function Edit( { attributes, setAttributes } ) { // hasConfirmed = l’utilisateur a validé l’ajout de l’iframe const initialConfirmed = Boolean( iframeData.url?.length && - isURL( iframeData.url ) && + isSafeIframeUrl( iframeData.url ) && iframeData.title?.length ); const [ hasConfirmed, setHasConfirmed ] = useState( initialConfirmed ); @@ -79,7 +82,7 @@ export default function Edit( { attributes, setAttributes } ) { // Validation const isIframeElligible = Boolean( iframeData.url?.length && - isURL( iframeData.url ) && + isSafeIframeUrl( iframeData.url ) && iframeData.title?.length ); diff --git a/src/blockparty-iframe/save.js b/src/blockparty-iframe/save.js index 4ebd8be..a27458b 100644 --- a/src/blockparty-iframe/save.js +++ b/src/blockparty-iframe/save.js @@ -5,7 +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'; +import { convertAttributesToRawProps } from './utils'; /** * The save function defines the way in which the different attributes should @@ -31,7 +31,7 @@ export default function save( { attributes } ) { title={ title } src={ url } loading={ lazyload ? 'lazy' : 'eager' } - { ...convertAttributesToProps( iframeAttributes ) } + { ...convertAttributesToRawProps( iframeAttributes ) } /> ); diff --git a/src/blockparty-iframe/utils.js b/src/blockparty-iframe/utils.js index ba6df66..f80f21e 100644 --- a/src/blockparty-iframe/utils.js +++ b/src/blockparty-iframe/utils.js @@ -31,13 +31,73 @@ export function isBooleanAttribute( attributeName ) { } /** - * Convert iframe attributes array to props object for React. - * Handles boolean attributes and React prop name mapping correctly. + * Iframe attribute names that can be added by editors. + * + * Any other attribute (event handlers such as `onload`, `srcdoc`, …) is + * dropped: block attributes are controlled by users who may not have the + * `unfiltered_html` capability. Keep in sync with BlockRenderer.php. + */ +const ALLOWED_ATTRIBUTES = [ + 'allow', + 'allowfullscreen', + 'allowpaymentrequest', + 'id', + 'referrerpolicy', + 'sandbox', +]; + +/** + * Check if an iframe attribute can be rendered. + * + * @param {string} attributeName - The name of the attribute to check. + * @return {boolean} True if the attribute is allowed, false otherwise. + */ +export function isAllowedIframeAttribute( attributeName ) { + if ( typeof attributeName !== 'string' ) { + return false; + } + + const name = attributeName.toLowerCase(); + + if ( ! /^[a-z][a-z0-9-]*$/.test( name ) ) { + return false; + } + + return ( + ALLOWED_ATTRIBUTES.includes( name ) || + /^(aria|data)-[a-z0-9-]+$/.test( name ) + ); +} + +/** + * Check if a URL can be used as iframe source (http or https only). + * + * @param {string} url - The URL to check. + * @return {boolean} True if the URL is safe, false otherwise. + */ +export function isSafeIframeUrl( url ) { + if ( typeof url !== 'string' || ! url.length ) { + return false; + } + + try { + const { protocol } = new URL( url ); + return protocol === 'http:' || protocol === 'https:'; + } catch ( error ) { + return false; + } +} + +/** + * Convert an iframe attributes array to a React props object, without filtering. + * + * Only meant for the deprecated save function, whose output must match the + * markup previously stored in post content and is never mounted in the DOM. * * @param {Array} attributes - Array of {key, value} objects. * @return {Object} Props object for React component. */ -export function convertAttributesToProps( attributes ) { +export function convertAttributesToRawProps( attributes ) { return Object.fromEntries( ( attributes || [] ).map( ( attr ) => { // Map HTML attribute name to React prop name @@ -55,29 +115,19 @@ export function convertAttributesToProps( attributes ) { } /** - * Check if an iframe attribute should be excluded. + * Convert iframe attributes array to props object for React. + * Drops non allowed attributes, handles boolean attributes and React prop + * name mapping. * - * @param {string} attributeName - The name of the attribute to check. - * @return {boolean} True if the attribute should be excluded, false otherwise. + * @param {Array} attributes - Array of {key, value} objects. + * @return {Object} Props object for React component. */ -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() ); +export function convertAttributesToProps( attributes ) { + return convertAttributesToRawProps( + ( attributes || [] ).filter( ( attr ) => + isAllowedIframeAttribute( attr?.key ) + ) + ); } /** @@ -98,11 +148,12 @@ export function parseIframeCode( value ) { // 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 + '' ); - const iframeElement = tempDiv.querySelector( 'iframe' ); + // Parse in an inert document so nothing is loaded or executed. + const doc = new window.DOMParser().parseFromString( + iframeTag + '', + 'text/html' + ); + const iframeElement = doc.querySelector( 'iframe' ); if ( ! iframeElement ) { return null; @@ -114,11 +165,11 @@ export function parseIframeCode( value ) { // Extract title attribute const title = iframeElement.getAttribute( 'title' ) || ''; - // Extract all other attributes (excluding managed and deprecated ones) + // Extract other allowed attributes only const attributes = []; for ( const attr of iframeElement.attributes ) { - if ( ! isExcludedIframeAttribute( attr.name ) ) { + if ( isAllowedIframeAttribute( attr.name ) ) { // For boolean attributes, store 'true' as value if present const attrValue = isBooleanAttribute( attr.name ) ? 'true' From ce015a3117af0a6cfcb63dd54e69b78e887ab06e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 17:51:54 +0000 Subject: [PATCH 3/4] chore(composer): unblock PHP CI dependency resolution - Bump wp-coding-standards/wpcs to 3.4.1: 3.3.0 is blocked by Composer because of security advisory PKSA-mh9b-91zm-m1gy (< 3.4.1). - Bump config.platform.php to 8.3.36: roave/security-advisories now conflicts with PHP 8.3 < 8.3.36. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011ft8SsWmmRXSa4kvxVHVdn --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2f69550..92608e0 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpro/grumphp-shim": true }, "platform": { - "php": "8.3.28" + "php": "8.3.36" } }, "require": { @@ -45,7 +45,7 @@ "roots/wordpress-no-content": "^6.0", "squizlabs/php_codesniffer": "^3.7", "wp-cli/wp-cli": "^2.6", - "wp-coding-standards/wpcs": "3.3.0" + "wp-coding-standards/wpcs": "3.4.1" }, "autoload": { "psr-4": { From bc012916b930cbbe42433d01501a0a687b8c339f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 17:56:45 +0000 Subject: [PATCH 4/4] chore: release 1.2.1 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011ft8SsWmmRXSa4kvxVHVdn --- .plugin-data | 2 +- .wordpress-org/blueprints/blueprint.json | 2 +- CHANGELOG.md | 7 ++++++- blockparty-iframe.php | 4 ++-- languages/blockparty-iframe.pot | 2 +- package-lock.json | 4 ++-- package.json | 2 +- readme.txt | 7 ++++++- src/blockparty-iframe/block.json | 2 +- 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.plugin-data b/.plugin-data index 57b5ffd..f1b8d4b 100644 --- a/.plugin-data +++ b/.plugin-data @@ -1,4 +1,4 @@ { - "version": "1.2.0", + "version": "1.2.1", "slug": "blockparty-iframe" } diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json index 6eb75c6..44f7b42 100644 --- a/.wordpress-org/blueprints/blueprint.json +++ b/.wordpress-org/blueprints/blueprint.json @@ -11,7 +11,7 @@ "pluginData": { "resource": "git:directory", "url": "https://github.com/BeAPI/blockparty-iframe", - "ref": "1.2.0", + "ref": "1.2.1", "refType": "tag" }, "options": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 547ed38..dd730ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [1.2.1] - 2026-11-05 ### Security - **Iframe attributes allowlist**: Only `allow`, `allowfullscreen`, `allowpaymentrequest`, `id`, `referrerpolicy`, `sandbox`, `aria-*` and `data-*` attributes are rendered, in the editor and on the front end. Event handlers (`onload`…), `srcdoc` and malformed attribute names are dropped, closing a stored XSS reachable by contributors. - **Iframe source protocols**: The iframe source must be an `http` or `https` URL, in the editor and on the front end (blocks `javascript:` URLs in the editor). +### Fixed + +- **Translations**: Add missing French translations. +- **Developer dependencies**: Bump `wp-coding-standards/wpcs` to 3.4.1 (security advisory on earlier versions) and `config.platform.php` to 8.3.36 so Composer can resolve dev dependencies in CI. + ## [1.2.0] - 2026-04-22 ### Changed diff --git a/blockparty-iframe.php b/blockparty-iframe.php index f214e34..c7b2f97 100644 --- a/blockparty-iframe.php +++ b/blockparty-iframe.php @@ -2,7 +2,7 @@ /** * Plugin Name: Blockparty Iframe * Description: Add a block to display an embedded frame in the WordPress editor. - * Version: 1.2.0 + * Version: 1.2.1 * Requires at least: 6.7 * Requires PHP: 8.1 * Author: Be API Technical team @@ -23,7 +23,7 @@ include_once __DIR__ . '/vendor/autoload.php'; } -define( 'BLOCKPARTY_IFRAME_VERSION', '1.2.0' ); +define( 'BLOCKPARTY_IFRAME_VERSION', '1.2.1' ); define( 'BLOCKPARTY_IFRAME_URL', plugin_dir_url( __FILE__ ) ); define( 'BLOCKPARTY_IFRAME_DIR', plugin_dir_path( __FILE__ ) ); define( 'BLOCKPARTY_IFRAME_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/languages/blockparty-iframe.pot b/languages/blockparty-iframe.pot index 4c4fb13..1ea4a78 100644 --- a/languages/blockparty-iframe.pot +++ b/languages/blockparty-iframe.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: Blockparty Iframe 1.2.0\n" +"Project-Id-Version: Blockparty Iframe 1.2.1\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/blockparty-iframe\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/package-lock.json b/package-lock.json index 62fdcf8..3679399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockparty-iframe", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockparty-iframe", - "version": "1.2.0", + "version": "1.2.1", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/block-editor": "15.17.0", diff --git a/package.json b/package.json index d43ae6e..b5b998f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockparty-iframe", - "version": "1.2.0", + "version": "1.2.1", "description": "Add a block to display an embedded frame in the WordPress editor.", "author": "Be API Technical team", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 4f98e3f..9b60c71 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: Be API Technical team Tags: block, iframe, editor Tested up to: 6.7 -Stable tag: 1.2.0 +Stable tag: 1.2.1 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -45,6 +45,11 @@ directory take precedence. For example, `/assets/screenshot-1.png` would win ove == Changelog == += 1.2.1 = +* Security: only allowlisted iframe attributes are rendered (allow, allowfullscreen, allowpaymentrequest, id, referrerpolicy, sandbox, aria-*, data-*). Event handlers, srcdoc and malformed attribute names are dropped, fixing a stored XSS reachable by contributors. +* Security: the iframe source must be an http or https URL, in the editor and on the front end. +* Translations: add missing French translations. + = 1.2.0 = * Dynamic block: iframe markup is rendered in PHP on the front end (block attributes are stored in post content). Existing posts with the previous static HTML remain supported in the editor. diff --git a/src/blockparty-iframe/block.json b/src/blockparty-iframe/block.json index 990fade..f31265e 100644 --- a/src/blockparty-iframe/block.json +++ b/src/blockparty-iframe/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "blockparty/iframe", - "version": "1.2.0", + "version": "1.2.1", "title": "Iframe", "category": "widgets", "description": "Display an embedded frame.",