| @@ -320,9 +320,9 @@ | ||
| 320 | 320 | $html .= esc_html( implode( ', ', $f['value'] ) ); |
| 321 | 321 | } else { |
| 322 | 322 | $f = $this->prepare_values( $f, WPF()->current_object['userid'] ); |
| 323 | 323 | $html .= '<div class="wpf-field-wrap">'; |
| 324 | - $html .= $f['value']; | |
| 324 | + $html .= ( is_scalar( $f['value'] ) && ! $this->is_display_value_safe_html( $f ) ) ? esc_html( (string) $f['value'] ) : $f['value']; | |
| 325 | 325 | $html .= '</div>'; |
| 326 | 326 | } |
| 327 | 327 | } else { |
| 328 | 328 | if( $default_values = wpforo_preg_grep_recursive( '#^\[.+?]$#isu', $f['values'] ) ) { |
| @@ -1216,14 +1216,35 @@ | ||
| 1216 | 1216 | $var = trim( (string) $var ); |
| 1217 | 1217 | if( ! strlen( (string) $var ) ) return []; |
| 1218 | 1218 | } |
| 1219 | 1219 | if( is_serialized( $var ) ) { |
| 1220 | - $var = unserialize( $var ); | |
| 1220 | + // This value can come straight from the request: the field state is | |
| 1221 | + // rebuilt from POST, and prepare_args() HTML-decodes it first, so an | |
| 1222 | + // entity-encoded payload arrives here fully restored. Never | |
| 1223 | + // instantiate classes, and never let one through as a value. | |
| 1224 | + $decoded = unserialize( $var, [ 'allowed_classes' => false ] ); | |
| 1225 | + if( is_array( $decoded ) ) { | |
| 1226 | + $var = $decoded; | |
| 1227 | + } elseif( is_scalar( $decoded ) ) { | |
| 1228 | + $var = [ $decoded ]; | |
| 1229 | + } else { | |
| 1230 | + return []; | |
| 1231 | + } | |
| 1221 | 1232 | } elseif( is_scalar( $var ) && strpos( (string) $var, $sep ) !== false ) { |
| 1222 | 1233 | $var = explode( $sep, $var ); |
| 1223 | 1234 | } |
| 1224 | - | |
| 1225 | - return array_map( 'trim', (array) $var ); | |
| 1235 | + | |
| 1236 | + // A multi-choice field value is a flat list of scalars. Only arrays and | |
| 1237 | + // objects are dropped, which a crafted payload can nest here and which | |
| 1238 | + // are a TypeError in trim() on PHP 8. Every other type is cast exactly | |
| 1239 | + // as trim() used to coerce it, so null and bool values are unchanged. | |
| 1240 | + $values = []; | |
| 1241 | + foreach( (array) $var as $key => $value ) { | |
| 1242 | + if( is_array( $value ) || is_object( $value ) ) continue; | |
| 1243 | + $values[ $key ] = trim( (string) $value ); | |
| 1244 | + } | |
| 1245 | + | |
| 1246 | + return $values; | |
| 1226 | 1247 | } |
| 1227 | 1248 | |
| 1228 | 1249 | public function build_array_using_string_rows( $string, $regexp = '' ) { |
| 1229 | 1250 | if( is_scalar( $string ) ) { |
| @@ -1294,8 +1315,24 @@ | ||
| 1294 | 1315 | |
| 1295 | 1316 | return $value; |
| 1296 | 1317 | } |
| 1297 | 1318 | |
| 1319 | + /** | |
| 1320 | + * Whether prepare_values()/esc_field() already turned $f['value'] into trusted, | |
| 1321 | + * pre-escaped HTML for this field type/name (so it must NOT be esc_html()'d again | |
| 1322 | + * before being echoed). | |
| 1323 | + * | |
| 1324 | + * @param array $f field arguments (after prepare_values()/esc_field()) | |
| 1325 | + * | |
| 1326 | + * @return bool | |
| 1327 | + */ | |
| 1328 | + public function is_display_value_safe_html( $f ) { | |
| 1329 | + $safe_types = [ 'url', 'email', 'tel', 'file', 'avatar', 'color', 'textarea', 'tinymce', 'datetime', 'html' ]; | |
| 1330 | + $safe_names = [ 'skype', 'location', 'signature', 'about' ]; | |
| 1331 | + | |
| 1332 | + return in_array( wpfval( $f, 'type' ), $safe_types, true ) || in_array( wpfval( $f, 'name' ), $safe_names, true ); | |
| 1333 | + } | |
| 1334 | + | |
| 1298 | 1335 | public function esc_field( $f ) { |
| 1299 | 1336 | if( wpfkey( $f, 'value' ) ) { |
| 1300 | 1337 | $f['value'] = wpforo_trim( $f['value'] ); |
| 1301 | 1338 | if( in_array( wpfval( $f, 'type' ), [ 'textarea', 'tinymce' ], true ) ) { |