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 classes/Visualizer/Gutenberg/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ public function get_visualizer_data( $post ) {
$data['visualizer-settings'] = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, $data['visualizer-settings'], $post_id, $data['visualizer-chart-type'] );

// handle data filter hooks
$data['visualizer-data'] = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_DATA, unserialize( html_entity_decode( get_the_content( $post_id ) ) ), $post_id, $data['visualizer-chart-type'] );
$data['visualizer-data'] = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_DATA, Visualizer_Module::decode_content( html_entity_decode( get_post_field( 'post_content', $post_id, 'raw' ) ) ), $post_id, $data['visualizer-chart-type'] );

Comment on lines 309 to 311
// we are going to format only for tabular charts, because we are not sure of the effect on others.
// this is to solve the case where boolean data shows up as all-ticks on gutenberg.
Expand Down
52 changes: 51 additions & 1 deletion classes/Visualizer/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,56 @@ final public static function get_features_for_license( $plan ) {
}
}

/**
* Safely unserialize chart/source content, blocking PHP object injection.
*
* Single guarded chokepoint shared by chart/source content sinks so the
* allowed_classes guard cannot be dropped from one call site independently.
*
* @param mixed $content The serialized content (only strings are decoded).
* @return mixed The decoded value (array for valid chart data), or false.
*/
public static function decode_content( $content ) {
if ( ! is_string( $content ) ) {
return false;
}
return self::strip_incomplete_objects( unserialize( trim( $content ), array( 'allowed_classes' => false ) ) );
}

/**
* Remove the __PHP_Incomplete_Class stubs the allowed_classes guard leaves
* behind; they crash map_deep() when the decoded value is written back to
* post meta. Legitimate chart content is nested arrays/scalars only.
*
* @param mixed $value The decoded value.
* @return mixed The value without object stubs; false for a top-level stub.
*/
private static function strip_incomplete_objects( $value ) {
if ( $value instanceof __PHP_Incomplete_Class ) {
return false;
}
if ( is_array( $value ) ) {
foreach ( $value as $key => $item ) {
if ( $item instanceof __PHP_Incomplete_Class ) {
unset( $value[ $key ] );
} elseif ( is_array( $item ) ) {
$value[ $key ] = self::strip_incomplete_objects( $item );
}
}
}
return $value;
}

/**
* Object-injection-safe drop-in for maybe_unserialize().
*
* @param mixed $value Raw meta/content value.
* @return mixed The decoded value for serialized input, the value unchanged otherwise.
*/
public static function maybe_decode_content( $value ) {
return is_serialized( $value ) ? self::decode_content( $value ) : $value;
}

/**
* Gets the chart content after common manipulations.
*/
Expand All @@ -793,7 +843,7 @@ function ( $matches ) {
},
$post_content
);
$data = unserialize( $post_content );
$data = self::decode_content( $post_content );
$altered = array();
if ( ! empty( $data ) ) {
foreach ( $data as $index => $array ) {
Expand Down
4 changes: 2 additions & 2 deletions classes/Visualizer/Module/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public function addRevision( $revision_id ) {
if ( $meta ) {
foreach ( $meta as $key => $value ) {
if ( 0 === strpos( $key, 'visualizer' ) ) {
add_metadata( 'post', $revision_id, $key, maybe_unserialize( $value[0] ) );
add_metadata( 'post', $revision_id, $key, self::maybe_decode_content( $value[0] ) );
}
}
}
Expand Down Expand Up @@ -251,7 +251,7 @@ public function restoreRevision( $post_id, $revision_id ) {
if ( $meta ) {
foreach ( $meta as $key => $value ) {
if ( 0 === strpos( $key, 'visualizer' ) ) {
add_post_meta( $post_id, $key, maybe_unserialize( $value[0] ) );
add_post_meta( $post_id, $key, self::maybe_decode_content( $value[0] ) );
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions classes/Visualizer/Module/Chart.php
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ public function renderChartPages() {
$chart_id = $new_chart_id;
foreach ( $post_meta as $key => $value ) {
if ( strpos( $key, 'visualizer-' ) !== false ) {
add_post_meta( $new_chart_id, $key, maybe_unserialize( $value[0] ) );
add_post_meta( $new_chart_id, $key, self::maybe_decode_content( $value[0] ) );
}
}
}
Expand Down Expand Up @@ -1364,8 +1364,8 @@ public function uploadData() {
if ( $source->fetch() ) {
$content = $source->getData( get_post_meta( $chart_id, Visualizer_Plugin::CF_EDITABLE_TABLE, true ) );
$populate = true;
if ( is_string( $content ) && is_array( unserialize( $content ) ) ) {
$json = unserialize( $content );
$json = self::decode_content( $content );
if ( is_array( $json ) ) {
// if source exists, so should data. if source exists but data is blank, do not populate the chart.
// if we populate the data even if it is empty, the chart will show "Table has no columns".
if ( array_key_exists( 'source', $json ) && ! empty( $json['source'] ) && ( ! array_key_exists( 'data', $json ) || empty( $json['data'] ) ) ) {
Expand Down Expand Up @@ -1456,7 +1456,7 @@ public function cloneChart() {
$post_meta = get_post_meta( $chart_id );
foreach ( $post_meta as $key => $value ) {
if ( strpos( $key, 'visualizer-' ) !== false ) {
add_post_meta( $new_chart_id, $key, maybe_unserialize( $value[0] ) );
add_post_meta( $new_chart_id, $key, self::maybe_decode_content( $value[0] ) );
}
}
$redirect = esc_url(
Expand Down
5 changes: 3 additions & 2 deletions classes/Visualizer/Module/Utility.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ private static function apply_chartjs_palette( array $settings, string $type, ar
// fall through.
case 'pie':
$chart = get_post( $chart_id );
$data = $chart instanceof WP_Post ? maybe_unserialize( $chart->post_content ) : array();
$raw = $chart instanceof WP_Post ? $chart->post_content : array();
$data = self::maybe_decode_content( $raw );
$name = 'slices';
$max = is_array( $data ) ? count( $data ) : $count;
// fall through.
Expand Down Expand Up @@ -434,7 +435,7 @@ private static function set_defaults_chartjs( $chart, $post_status ) {
case 'polarArea':
// fall through.
case 'pie':
$data = unserialize( $chart->post_content );
$data = self::decode_content( $chart->post_content );
$name = 'slices';
$max = count( $data );
// fall through.
Expand Down
2 changes: 1 addition & 1 deletion classes/Visualizer/Module/Wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private function setup_wizard_import_chart() {
update_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, $series );
update_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, '' );

$data = maybe_unserialize( $data );
$data = Visualizer_Module::decode_content( $data );
$setting_series = array();
$setting_slices = array();
foreach ( $data as $s ) {
Expand Down
2 changes: 1 addition & 1 deletion classes/Visualizer/Source/Csv/Remote.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private function _repopulate( $chart_id ) {
// if filename is empty, extract it from chart content
if ( empty( $this->_filename ) ) {
$chart = get_post( $chart_id );
$data = unserialize( html_entity_decode( $chart->post_content ) );
$data = Visualizer_Module::decode_content( html_entity_decode( $chart->post_content ) );
if ( ! isset( $data['source'] ) ) {
return false;
}
Expand Down
32 changes: 32 additions & 0 deletions tests/test-ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,38 @@ public function test_json_get_roots_blocks_link_local_for_contributor() {
$this->assertSame( 0, $requests );
}

/**
* Test that the setup wizard import step builds per-row settings from the
* decoded sample data, covering the decode_content() call in the wizard.
*/
public function test_wizard_import_chart_builds_settings_from_decoded_sample_data() {
wp_set_current_user( $this->admin_user_id );

$_POST = array(
'security' => wp_create_nonce( VISUALIZER_ABSPATH ),
'step' => 'step_2',
'chart_type' => 'pie',
);

try {
$this->_handleAjax( 'visualizer_wizard_step_process' );
} catch ( WPAjaxDieContinueException $e ) {
// We expected this, do nothing.
}

// Skip any PHP notices emitted before the JSON payload.
$response = json_decode( substr( $this->_last_response, (int) strpos( $this->_last_response, '{' ) ) );
$this->assertSame( 1, $response->success );

// Data rows in the bundled sample = total lines minus label + type rows.
$expected_rows = count( array_filter( array_map( 'trim', file( VISUALIZER_ABSPATH . '/samples/pie.csv' ) ) ) ) - 2;
$settings = get_post_meta( $response->chart_id, Visualizer_Plugin::CF_SETTINGS, true );

$this->assertGreaterThan( 0, $expected_rows );
$this->assertCount( $expected_rows, $settings['series'] );
$this->assertCount( $expected_rows, $settings['slices'] );
}

/**
* Utility method to mock pro version.
*/
Expand Down
Loading
Loading