Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ 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`), in the editor and on the front end. 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 on the front end with the `blockparty_iframe_allowed_features` filter.
- **`allowpaymentrequest` removed**: This obsolete attribute is no longer rendered.

## [1.2.1] - 2026-11-05

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

/**
* 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.
* Keep in sync with utils.js.
*
* @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.
*
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 Down
53 changes: 49 additions & 4 deletions src/blockparty-iframe/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,50 @@ export function isBooleanAttribute( attributeName ) {
const ALLOWED_ATTRIBUTES = [
'allow',
'allowfullscreen',
'allowpaymentrequest',
'id',
'referrerpolicy',
'sandbox',
];

/**
* Permissions Policy features that editors can delegate through `allow`.
*
* Sensitive features (camera, microphone, geolocation, display-capture,
* payment…) are left out. Keep in sync with BlockRenderer.php.
*/
const ALLOWED_FEATURES = [
'accelerometer',
'autoplay',
'clipboard-write',
'encrypted-media',
'fullscreen',
'gyroscope',
'picture-in-picture',
'web-share',
];

/**
* Keep only allowlisted features from an `allow` Permissions Policy value.
* Origin lists are dropped, so each feature defaults to the iframe origin.
*
* @param {string} value - Raw `allow` attribute value.
* @return {string} Sanitized value, empty when no feature is kept.
*/
export function sanitizeAllowValue( value ) {
if ( typeof value !== 'string' ) {
return '';
}

const features = value
.split( ';' )
.map( ( directive ) =>
( directive.trim().split( /\s+/ )[ 0 ] || '' ).toLowerCase()
)
.filter( ( name ) => ALLOWED_FEATURES.includes( name ) );

return [ ...new Set( features ) ].join( '; ' );
}

/**
* Check if an iframe attribute can be rendered.
*
Expand Down Expand Up @@ -124,9 +162,16 @@ export function convertAttributesToRawProps( attributes ) {
*/
export function convertAttributesToProps( attributes ) {
return convertAttributesToRawProps(
( attributes || [] ).filter( ( attr ) =>
isAllowedIframeAttribute( attr?.key )
)
( attributes || [] )
.filter( ( attr ) => isAllowedIframeAttribute( attr?.key ) )
.map( ( attr ) =>
attr.key.toLowerCase() === 'allow'
? { ...attr, value: sanitizeAllowValue( attr.value ) }
: attr
)
.filter(
( attr ) => attr.key.toLowerCase() !== 'allow' || attr.value
)
);
}

Expand Down
Loading