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
23 changes: 21 additions & 2 deletions src/block/heading/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
Transform,
useUniqueId,
} from '~stackable/block-components'
import { version as VERSION, i18n } from 'stackable'
import {
version as VERSION,
i18n,
settings,
} from 'stackable'
import classnames from 'classnames'
import { kebabCase } from 'lodash'
import {
Expand Down Expand Up @@ -75,13 +79,28 @@ const Edit = props => {
attributes,
} = props

const { parentBlock } = useSelect( select => {
const { parentBlock, postType } = useSelect( select => {
const { getBlockRootClientId, getBlock } = select( 'core/block-editor' )
const parentClientId = getBlockRootClientId( props.clientId )
return {
parentBlock: getBlock( parentClientId ),
postType: select( 'core/editor' ).getCurrentPostType(),
}
}, [ props.clientId ] )

// Set useThemeTextMargins default value from setting on first load
useEffect( () => {
if ( attributes.useThemeTextMargins === undefined || attributes.useThemeTextMargins === '' ) {
const isPost = postType === 'post'

const defaultThemeMargins = isPost
? !! settings.stackable_enable_heading_default_theme_margins_posts
: !! settings.stackable_enable_heading_default_theme_margins_non_posts

setAttributes( { useThemeTextMargins: defaultThemeMargins } )
}
}, [] )

const textClasses = getTypographyClasses( props.attributes )
const blockAlignmentClass = getAlignmentClasses( props.attributes )
const blockClassNames = classnames( [
Expand Down
15 changes: 13 additions & 2 deletions src/block/icon-list/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import blockStyles from './style'
* External dependencies
*/
import classnames from 'classnames'
import { i18n, version as VERSION } from 'stackable'
import {
i18n,
version as VERSION,
settings,
} from 'stackable'
import {
InspectorTabs,
InspectorStyleControls,
Expand Down Expand Up @@ -50,7 +54,7 @@ import { compose } from '@wordpress/compose'
import { useInnerBlocksProps } from '@wordpress/block-editor'
import { dispatch, useSelect } from '@wordpress/data'
import { addFilter } from '@wordpress/hooks'
import { memo } from '@wordpress/element'
import { memo, useEffect } from '@wordpress/element'
import { useBlockLayoutDefaults } from '~stackable/hooks'
import { ToggleControl } from '@wordpress/components'

Expand Down Expand Up @@ -201,6 +205,13 @@ const Edit = props => {
version: VERSION,
} )

// Set icon default value from setting on first load
useEffect( () => {
if ( attributes.icon === undefined || attributes.icon === '' ) {
setAttributes( { icon: settings.stackable_icon_list_block_default_icon || DEFAULT_SVG } )
}
}, [] )

return (
<>
<InspectorControls
Expand Down
4 changes: 2 additions & 2 deletions src/block/icon-list/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from '~stackable/block-components'
import { AttributeObject } from '~stackable/util'
import { version as VERSION } from 'stackable'
import { DEFAULT_SVG } from './util'

export const iconListAttributes = {
// Columns.
Expand Down Expand Up @@ -51,9 +50,10 @@ export const iconListAttributes = {
},

// Icon.
// Changed to empty string to detect if the attribute is unset (intially created).
icon: {
type: 'string',
default: DEFAULT_SVG,
default: '',
},
markerColor: {
type: 'string',
Expand Down
3 changes: 3 additions & 0 deletions src/components/admin-icon-setting/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ugb-admin-icon-setting {
--wp-components-color-accent: var(--stk-skin-dark);
}
32 changes: 32 additions & 0 deletions src/components/admin-icon-setting/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import AdminBaseSetting from '../admin-base-setting'
import IconControl from '../icon-control'
import classnames from 'classnames'

const defaultIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190 190"><polygon points="173.8,28.4 60.4,141.8 15.7,97.2 5.1,107.8 60.4,163 184.4,39 173.8,28.4"/></svg>'

const AdminIconSetting = props => {
return (
<AdminBaseSetting
{ ...props }
className={ classnames( props.className, 'ugb-admin-icon-setting' ) }>
<IconControl
label=""
value={ props.value }
defaultValue={ defaultIcon }
onChange={ icon => {
props.onChange( icon )
} }
allowReset={ false }
hasPanelModifiedIndicator={ true }
/>
{ props.children }
</AdminBaseSetting>
)
}

AdminIconSetting.defaultProps = {
value: '',
onChange: () => {},
}

export default AdminIconSetting
3 changes: 2 additions & 1 deletion src/components/icon-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const IconControl = props => {
<BaseControl
className="ugb-icon-control stk-control"
{ ...omit( props, [ 'onChange', 'value' ] ) }
allowReset={ true }
allowReset={ props.allowReset }
value={ props.value }
defaultValue={ props.defaultValue }
onChange={ props.onChange }
Expand Down Expand Up @@ -71,6 +71,7 @@ IconControl.defaultProps = {
returnSVGValue: true, // If true, the value provided in onChange will be the SVG markup of the icon. If false, the value will be a prefix-iconName value.
onChange: () => {},
defaultValue: '',
allowReset: true,
hasPanelModifiedIndicator: true,
}

Expand Down
53 changes: 53 additions & 0 deletions src/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,62 @@ public function register_settings() {
'default' => false,
)
);

register_setting(
'stackable_editor_settings',
'stackable_enable_heading_default_theme_margins_posts',
array(
'type' => 'boolean',
'description' => __( "When enabled, newly added Stackable Heading blocks in Posts will use the theme's default margins automatically.", STACKABLE_I18N ),
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
'default' => false,
)
);

register_setting(
'stackable_editor_settings',
'stackable_enable_heading_default_theme_margins_non_posts',
array(
'type' => 'boolean',
'description' => __( "When enabled, newly added Stackable Heading blocks in non-Post content (Pages and custom post types) will use the theme's default margins automatically.", STACKABLE_I18N ),
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
'default' => false,
)
);

register_setting(
'stackable_editor_settings',
'stackable_icon_list_block_default_icon',
array(
'type' => 'string',
'description' => __( 'Choose the default icon that will be used when adding a new Icon List block.', STACKABLE_I18N ),
'sanitize_callback' => array( $this, 'sanitize_svg_setting' ),
'show_in_rest' => true,
'default' => '',
)
);
}

public function sanitize_array_setting( $input ) {
return ! is_array( $input ) ? array( array() ) : $input;
}

public function sanitize_svg_setting( $input ) {
if ( empty( $input ) ) {
return '';
}

// Remove scripts, event handlers, foreignObject, iframe, embeds
$input = preg_replace( '/<\s*(script|iframe|embed|object|foreignObject)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $input );
$input = preg_replace( '/on\w+\s*=\s*"[^"]*"/i', '', $input );
$input = preg_replace( "/on\w+\s*=\s*'[^']*'/i", '', $input );
$input = preg_replace( '/javascript:/i', '', $input );

return $input;
}
Comment on lines +286 to +298
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SVG sanitization may be incomplete for security-critical use.

The current implementation provides basic protection but has gaps that could allow XSS:

  1. Unquoted event handlers: The regex only handles quoted attributes (on\w+=\s*"..." and '...'), missing unquoted values like onclick=alert(1).

  2. Missing dangerous patterns:

    • data: URLs (e.g., xlink:href="data:text/html,<script>...")
    • xlink:href and href attributes pointing to javascript:
    • <use> elements referencing external content
    • <animate>, <set> elements that can trigger scripts
  3. Error handling: preg_replace returns null on error; this should be handled.

Proposed improvements for more robust sanitization
 public function sanitize_svg_setting( $input ) {
 	if ( empty( $input ) ) {
 		return '';
 	}

 	// Remove scripts, event handlers, foreignObject, iframe, embeds
 	$input = preg_replace( '/<\s*(script|iframe|embed|object|foreignObject)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $input );
+	// Remove potentially dangerous elements
+	$input = preg_replace( '/<\s*(use|animate|set|animateTransform)[^>]*\/?>/is', '', $input );
 	$input = preg_replace( '/on\w+\s*=\s*"[^"]*"/i', '', $input );
 	$input = preg_replace( "/on\w+\s*=\s*'[^']*'/i", '', $input );
+	// Handle unquoted event handlers
+	$input = preg_replace( '/on\w+\s*=\s*[^\s>]+/i', '', $input );
 	$input = preg_replace( '/javascript:/i', '', $input );
+	// Remove data: URLs and xlink:href with dangerous protocols
+	$input = preg_replace( '/xlink:href\s*=\s*["\'][^"\']*(?:javascript:|data:)[^"\']*["\']/i', '', $input );
+	$input = preg_replace( '/href\s*=\s*["\'][^"\']*(?:javascript:|data:)[^"\']*["\']/i', '', $input );

+	// Handle preg_replace errors
+	if ( $input === null ) {
+		return '';
+	}
+
 	return $input;
 }

Alternatively, consider using WordPress's built-in wp_kses with an SVG-specific allowed tags/attributes list, or a dedicated SVG sanitization library for more comprehensive protection.

🤖 Prompt for AI Agents
In `@src/editor-settings.php` around lines 286 - 298, sanitize_svg_setting
currently uses brittle regexes that miss unquoted event handlers, data: URLs,
xlink/href javascript targets, <use> references and animation elements, and does
not handle preg_replace returning null; update it to perform robust sanitization
by either (A) replacing the ad-hoc regex approach in sanitize_svg_setting with
WordPress's wp_kses using a restrictive SVG-specific allowed tags/attributes
whitelist (including removal of on* attributes regardless of quoting, stripping
href/xlink:href values that start with javascript: or data:, and disallowing
<use>, <animate>, <set>), or (B) if you keep regexes, add patterns to remove
unquoted on* attributes, strip any href/xlink:href attributes whose values begin
with javascript: or data:, remove <use>, <animate>, <set> elements, and after
each preg_replace check for null and handle by returning an empty string or
logging and returning safe output; make these changes inside the
sanitize_svg_setting function to ensure all dangerous patterns are covered.


/**
* Make our settings available in the editor.
*
Expand All @@ -267,6 +317,9 @@ public function add_settings( $settings ) {
$settings['stackable_enable_reset_layout'] = get_option( 'stackable_enable_reset_layout' );
$settings['stackable_enable_save_as_default_block'] = get_option( 'stackable_enable_save_as_default_block' );
$settings['stackable_enable_text_default_block'] = get_option( 'stackable_enable_text_default_block' );
$settings['stackable_enable_heading_default_theme_margins_posts'] = get_option( 'stackable_enable_heading_default_theme_margins_posts' );
$settings['stackable_enable_heading_default_theme_margins_non_posts'] = get_option( 'stackable_enable_heading_default_theme_margins_non_posts' );
$settings['stackable_icon_list_block_default_icon'] = get_option( 'stackable_icon_list_block_default_icon' );

return $settings;
}
Expand Down
43 changes: 43 additions & 0 deletions src/welcome/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import AdminSelectSetting from '~stackable/components/admin-select-setting'
import AdminToggleSetting from '~stackable/components/admin-toggle-setting'
import AdminTextSetting from '~stackable/components/admin-text-setting'
import AdminToolbarSetting from '~stackable/components/admin-toolbar-setting'
import AdminIconSetting from '~stackable/components/admin-icon-setting'
import { GettingStarted } from './getting-started'
import { BLOCK_STATE } from '~stackable/util/blocks'
import { BlockToggler, OptimizationSettings } from '~stackable/deprecated/v2/welcome/admin'
Expand Down Expand Up @@ -81,6 +82,14 @@ const SEARCH_TREE = [
__( 'Nested Wide Block Width', i18n ),
],
},
{
id: 'block-defaults',
children: [
__( 'Default to Theme Margins for Headings (Posts)', i18n ),
__( 'Default to Theme Margins for Headings (Non-Posts)', i18n ),
__( 'Default Icon for Icon List Block', i18n ),
],
},
{
id: 'editor',
children: [
Expand Down Expand Up @@ -631,6 +640,7 @@ const EditorSettings = props => {

const groups = filteredSearchTree.find( tab => tab.id === 'editor-settings' ).groups
const blocks = groups.find( group => group.id === 'blocks' )
const blockDefaults = groups.find( group => group.id === 'block-defaults' )
const editor = groups.find( group => group.id === 'editor' )
const toolbar = groups.find( group => group.id === 'toolbar' )
const inspector = groups.find( group => group.id === 'inspector' )
Expand Down Expand Up @@ -668,6 +678,39 @@ const EditorSettings = props => {
/>
</div>
}
{ ( blockDefaults.children === null || blockDefaults.children.length > 0 ) &&
<div className="s-setting-group">
<h2>{ __( 'Block Defaults', i18n ) }</h2>
<p className="s-settings-subtitle">{ __( 'Adjust the default behavior of some Stackable blocks.', i18n ) }</p>
<AdminToggleSetting
label={ __( 'Default to Theme Margins for Headings (Posts)', i18n ) }
searchedSettings={ blockDefaults.children }
value={ settings.stackable_enable_heading_default_theme_margins_posts }
onChange={ value => {
handleSettingsChange( { stackable_enable_heading_default_theme_margins_posts: value } ) // eslint-disable-line camelcase
} }
help={ __( "When enabled, newly added Stackable Heading blocks in Posts will use the theme's default margins automatically. Existing blocks are not affected.", i18n ) }
/>
<AdminToggleSetting
label={ __( 'Default to Theme Margins for Headings (Non-Posts)', i18n ) }
searchedSettings={ blockDefaults.children }
value={ settings.stackable_enable_heading_default_theme_margins_non_posts }
onChange={ value => {
handleSettingsChange( { stackable_enable_heading_default_theme_margins_non_posts: value } ) // eslint-disable-line camelcase
} }
help={ __( "When enabled, newly added Stackable Heading blocks in non-Post content (Pages and custom post types) will use the theme's default margins automatically. Existing blocks are not affected.", i18n ) }
/>
<AdminIconSetting
label={ __( 'Default Icon for Icon List Block', i18n ) }
searchedSettings={ blockDefaults.children }
value={ settings.stackable_icon_list_block_default_icon }
onChange={ value => {
handleSettingsChange( { stackable_icon_list_block_default_icon: value } ) // eslint-disable-line camelcase
} }
help={ __( 'Choose the default icon that will be used when adding a new Icon List block.', i18n ) }
/>
</div>
}
{ ( editor.children === null || editor.children.length > 0 ) &&
<div className="s-setting-group">
<h2>{ __( 'Editor', i18n ) }</h2>
Expand Down
Loading