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
2 changes: 1 addition & 1 deletion .plugin-data
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"version": "1.2.0",
"version": "1.2.1",
"slug": "blockparty-iframe"
}
2 changes: 1 addition & 1 deletion .wordpress-org/blueprints/blueprint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +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/).

## [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
Expand Down
4 changes: 2 additions & 2 deletions blockparty-iframe.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__ ) );
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"phpro/grumphp-shim": true
},
"platform": {
"php": "8.3.28"
"php": "8.3.36"
}
},
"require": {
Expand All @@ -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": {
Expand Down
72 changes: 47 additions & 25 deletions includes/BlockRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@

class BlockRenderer {

/**
* URL protocols allowed for the iframe source.
*
* @var string[]
*/
private const ALLOWED_PROTOCOLS = [ 'http', 'https' ];

/**
* Iframe attribute names that can be added by editors.
*
* Any other attribute (event handlers such as `onload`, `srcdoc`, …) is
* dropped, since block attributes are controlled by users who may not
* have the `unfiltered_html` capability.
*
* @var string[]
*/
private const ALLOWED_ATTRIBUTES = [
'allow',
'allowfullscreen',
'allowpaymentrequest',
'id',
'referrerpolicy',
'sandbox',
];

/**
* Render the iframe block on the front end.
*
Expand All @@ -19,7 +44,9 @@ public static function render( $attributes ) {
? $attributes['iframeAttributes']
: [];

if ( '' === $url || '' === $title ) {
$safe_url = '' !== $url ? esc_url( $url, self::ALLOWED_PROTOCOLS ) : '';

if ( '' === $safe_url || '' === $title ) {
return sprintf(
'<div %s></div>',
get_block_wrapper_attributes()
Expand All @@ -32,7 +59,7 @@ public static function render( $attributes ) {
$iframe_html = sprintf(
'<iframe title="%1$s" src="%2$s" loading="%3$s"%4$s></iframe>',
esc_attr( $title ),
esc_url( $url ),
$safe_url,
esc_attr( $loading ),
$extra_attrs
);
Expand All @@ -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 )
);
}
Expand All @@ -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 );
}
}
Original file line number Diff line number Diff line change
@@ -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"]}}}
{"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 <iframe src=\"https:\/\/\u2026\"":["https:\/\/\u2026 ou <iframe src=\"https:\/\/\u2026\""],"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"]}}}
Binary file modified languages/blockparty-iframe-fr_FR.mo
Binary file not shown.
10 changes: 7 additions & 3 deletions languages/blockparty-iframe-fr_FR.po
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ msgstr ""
"Project-Id-Version: Blockparty Iframe 1.0.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/blockparty-"
"iframe\n"
"POT-Creation-Date: 2026-01-12T10:29:55+00:00\n"
"PO-Revision-Date: 2026-01-12 11:32+0100\n"
"POT-Creation-Date: 2026-07-02T15:00:50+00:00\n"
"PO-Revision-Date: 2026-07-02 17:01+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.8\n"
"X-Generator: Poedit 3.9\n"
"X-Domain: blockparty-iframe\n"

#. Plugin Name of the plugin
Expand Down Expand Up @@ -58,6 +58,10 @@ msgstr "Veuillez remplir la source du cadre ainsi que son titre."
msgid "Source"
msgstr "Source"

#: build/blockparty-iframe/index.js:1
msgid "https://… or <iframe src=\"https://…\""
msgstr "https://… ou <iframe src=\"https://…\""

#: build/blockparty-iframe/index.js:1
msgid "You can either paste a URL or the iframe code."
msgstr "Vous pouvez soit renseigner une URL soit le code HTML du cadre."
Expand Down
8 changes: 6 additions & 2 deletions languages/blockparty-iframe.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Blockparty Iframe 1.1.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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-01-12T10:29:55+00:00\n"
"POT-Creation-Date: 2026-07-02T15:00:50+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.11.0\n"
"X-Domain: blockparty-iframe\n"
Expand Down Expand Up @@ -57,6 +57,10 @@ msgstr ""
msgid "Source"
msgstr ""

#: build/blockparty-iframe/index.js:1
msgid "https://… or <iframe src=\"https://…\""
msgstr ""

#: build/blockparty-iframe/index.js:1
msgid "You can either paste a URL or the iframe code."
msgstr ""
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 6 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
7 changes: 2 additions & 5 deletions src/blockparty-iframe/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
"$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.",
"example": {},
"supports": {
"align": [
"wide",
"full"
],
"align": [ "wide", "full" ],
"dimensions": {
"aspectRatio": true,
"height": true,
Expand Down
16 changes: 11 additions & 5 deletions src/blockparty-iframe/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
ToolbarGroup,
ToolbarButton,
} from '@wordpress/components';
import { isURL } from '@wordpress/url';

import { useState } from '@wordpress/element';

Expand All @@ -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
Expand Down Expand Up @@ -71,15 +74,15 @@ 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 );

// Validation
const isIframeElligible = Boolean(
iframeData.url?.length &&
isURL( iframeData.url ) &&
isSafeIframeUrl( iframeData.url ) &&
iframeData.title?.length
);

Expand Down Expand Up @@ -160,7 +163,10 @@ export default function Edit( { attributes, setAttributes } ) {
label={ __( 'Source', 'blockparty-iframe' ) }
value={ iframeData.url }
onChange={ handleUrlChange }
placeholder={ `https://... or <iframe src="https://..."` }
placeholder={ __(
'https://… or <iframe src="https://…"',
'blockparty-iframe'
) }
help={ __(
'You can either paste a URL or the iframe code.',
'blockparty-iframe'
Expand Down
4 changes: 2 additions & 2 deletions src/blockparty-iframe/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,7 +31,7 @@ export default function save( { attributes } ) {
title={ title }
src={ url }
loading={ lazyload ? 'lazy' : 'eager' }
{ ...convertAttributesToProps( iframeAttributes ) }
{ ...convertAttributesToRawProps( iframeAttributes ) }
/>
</div>
);
Expand Down
Loading
Loading