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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ 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]

### Security

- **Iframe `allow` policy**: The `allow` attribute value is filtered against an allowlist of low-risk Permissions Policy features (`accelerometer`, `autoplay`, `clipboard-write`, `encrypted-media`, `fullscreen`, `gyroscope`, `picture-in-picture`, `web-share`). Origin lists are dropped, so features only apply to the iframe origin. Users without `unfiltered_html` can no longer delegate camera, microphone, geolocation, screen capture or payment to an embedded page. The list can be extended with the `blockparty_iframe_allowed_features` filter.
- **`allowpaymentrequest` removed**: This obsolete attribute is no longer rendered.

### Changed

- **Editor preview rendered on the server**: The editor preview uses `ServerSideRender`, so it goes through `BlockRenderer` like the front end. Iframe attribute filtering (and the `blockparty_iframe_allowed_features` filter) now lives in PHP only; the duplicated JS allowlists are removed. Adds the `@wordpress/server-side-render` dependency.

## [1.2.1] - 2026-11-05

### Security
Expand Down
72 changes: 64 additions & 8 deletions includes/BlockRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,34 @@ class BlockRenderer {
private const ALLOWED_ATTRIBUTES = [
'allow',
'allowfullscreen',
'allowpaymentrequest',
'id',
'referrerpolicy',
'sandbox',
];

/**
* Render the iframe block on the front end.
* Permissions Policy features that editors can delegate through `allow`.
*
* Sensitive features (camera, microphone, geolocation, display-capture,
* payment…) are left out: users without `unfiltered_html` must not be
* able to grant them to a third-party document on behalf of the site.
*
* @var string[]
*/
private const ALLOWED_FEATURES = [
'accelerometer',
'autoplay',
'clipboard-write',
'encrypted-media',
'fullscreen',
'gyroscope',
'picture-in-picture',
'web-share',
];

/**
* Render the iframe block, on the front end and in the editor preview
* (ServerSideRender), so attribute filtering only lives here.
*
* @param array $attributes Block attributes.
*
Expand Down Expand Up @@ -93,6 +113,14 @@ private static function build_iframe_attribute_string( array $attributes ): stri
continue;
}

if ( 'allow' === $key ) {
$value = self::sanitize_allow_value( $value );

if ( '' === $value ) {
continue;
}
}

if ( self::is_boolean_attribute( $key ) ) {
if ( 'true' === $value || '' === $value ) {
$html .= ' ' . $key;
Expand Down Expand Up @@ -120,12 +148,42 @@ private static function build_iframe_attribute_string( array $attributes ): stri
private static function is_boolean_attribute( string $attribute_name ): bool {
$boolean_attrs = [
'allowfullscreen',
'allowpaymentrequest',
];

return in_array( strtolower( $attribute_name ), $boolean_attrs, true );
}

/**
* Keep only allowlisted features from an `allow` Permissions Policy value.
*
* Origin lists are dropped, so each kept feature defaults to the iframe
* source origin only.
*
* @param string $value Raw `allow` attribute value.
*
* @return string Sanitized value, empty when no feature is kept.
*/
private static function sanitize_allow_value( string $value ): string {
/**
* Filters the Permissions Policy features that editors can delegate to an iframe.
*
* @param string[] $features Allowed feature names.
*/
$features = (array) apply_filters( 'blockparty_iframe_allowed_features', self::ALLOWED_FEATURES );
$kept = [];

foreach ( explode( ';', $value ) as $directive ) {
$parts = preg_split( '/\s+/', trim( $directive ) );
$name = is_array( $parts ) ? strtolower( $parts[0] ) : '';

if ( '' !== $name && in_array( $name, $features, true ) ) {
$kept[ $name ] = $name;
}
}

return implode( '; ', $kept );
}

/**
* Check if an iframe attribute can be output.
*
Expand All @@ -141,10 +199,8 @@ private static function is_allowed_iframe_attribute( string $attribute_name ): b
return false;
}

if ( in_array( $attribute_name, self::ALLOWED_ATTRIBUTES, true ) ) {
return true;
}

return 1 === preg_match( '/^(aria|data)-[a-z0-9-]+$/', $attribute_name );
return in_array( $attribute_name, self::ALLOWED_ATTRIBUTES, true )
|| str_starts_with( $attribute_name, 'aria-' )
|| str_starts_with( $attribute_name, 'data-' );
}
}
87 changes: 76 additions & 11 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"@wordpress/components": "32.6.0",
"@wordpress/element": "6.44.0",
"@wordpress/i18n": "6.17.0",
"@wordpress/icons": "^12.2.0"
"@wordpress/icons": "^12.2.0",
"@wordpress/server-side-render": "6.20.0"
},
"volta": {
"node": "24.15.0"
Expand Down
31 changes: 19 additions & 12 deletions src/blockparty-iframe/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import {

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

/**
* Default export: the named `ServerSideRender` export only exists in the
* `wp.serverSideRender` global since WordPress 6.9, and the plugin supports 6.7.
*/
import ServerSideRender from '@wordpress/server-side-render';

/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* Those files can contain any CSS code that gets applied to the editor.
Expand All @@ -38,11 +44,7 @@ import { useState } from '@wordpress/element';
import './editor.scss';

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

/**
* The edit function describes the structure of your block in the context of the
Expand Down Expand Up @@ -86,6 +88,10 @@ export default function Edit( { attributes, setAttributes } ) {
iframeData.title?.length
);

// Block supports (align, dimensions) already style the editor wrapper:
// skipBlockSupportAttributes keeps `style.dimensions`, so drop them here.
const { align, style, ...previewAttributes } = attributes;

const showPlaceholder = ! hasConfirmed;
const showIframe = hasConfirmed && isIframeElligible;

Expand Down Expand Up @@ -197,13 +203,14 @@ export default function Edit( { attributes, setAttributes } ) {
) }

{ showIframe && (
<iframe
title={ iframeData.title }
src={ iframeData.url }
loading={ lazyload ? 'lazy' : 'eager' }
{ ...convertAttributesToProps(
iframeData.iframeAttributes
) }
// Preview with the front-end renderer so iframe attributes are
// filtered in one place only (BlockRenderer.php). Use the form
// state, which "View" shows before "Add iframe" saves it.
<ServerSideRender
block="blockparty/iframe"
attributes={ { ...previewAttributes, ...iframeData } }
className="blockparty-iframe__preview"
skipBlockSupportAttributes
/>
) }
</div>
Expand Down
12 changes: 12 additions & 0 deletions src/blockparty-iframe/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,16 @@
pointer-events: none;
touch-action: none;
}

// The server-side rendered preview nests the front-end markup inside the
// block wrapper, which carries the dimensions: make it fill the wrapper.
// Nested selector needed to beat the front-end default aspect ratio.
.blockparty-iframe__preview {
height: 100%;

> .wp-block-blockparty-iframe {
aspect-ratio: auto;
height: 100%;
}
}
}
Loading
Loading