Headline
CVE-2023-4520: Changeset 2957322 for fv-wordpress-flowplayer – WordPress Plugin Repository
The FV Flowplayer Video Player plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the ‘_fv_player_user_video’ parameter saved via the ‘save’ function hooked via init, and the plugin is also vulnerable to Arbitrary Usermeta Update via the ‘save’ function in versions up to, and including, 7.5.37.7212 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page, and makes it possible to update the user metas arbitrarily, but the meta value can only be a string.
fv-wordpress-flowplayer/trunk/flowplayer.php
r2946640
r2957322
4
4
Plugin URI: http://foliovision.com/wordpress/plugins/fv-wordpress-flowplayer
5
5
Description: Formerly FV WordPress Flowplayer. Supports MP4, HLS, MPEG-DASH, WebM and OGV. Advanced features such as overlay ads or popups. Uses Flowplayer 7.2.8.
6
Version: 7.5.37.7212
6
Version: 7.5.39.7212
7
7
Author URI: http://foliovision.com/
8
8
License: GPL-3.0
…
…
32
32
33
33
global $fv_wp_flowplayer_ver;
34
$fv_wp_flowplayer_ver = '7.5.37.7212’;
34
$fv_wp_flowplayer_ver = '7.5.39.7212’;
35
35
$fv_wp_flowplayer_core_ver = '7.2.12.3’;
36
36
include_once( dirname( __FILE__ ) . ‘/includes/extra-functions.php’ );
…
…
133
133
}
134
134
}
135
136
add_filter( 'tables_to_repair’, ‘fv_player_tables_to_repair’ );
137
138
function fv_player_tables_to_repair( $tables ) {
139
global $wpdb;
140
141
$tables[] = FV_Player_Db_Player::get_db_table_name();
142
$tables[] = FV_Player_Db_Player_Meta::get_db_table_name();
143
$tables[] = FV_Player_Db_Video::get_db_table_name();
144
$tables[] = FV_Player_Db_Video_Meta::get_db_table_name();
145
$tables[] = FV_Player_Stats::get_table_name();
146
$tables[] = $wpdb->prefix . 'fv_player_emails’;
147
$tables[] = $wpdb->prefix . 'fv_player_encoding_jobs’;
148
$tables[] = $wpdb->prefix . 'fv_fp_hls_access_tokens’;
149
150
return $tables;
151
}
fv-wordpress-flowplayer/trunk/models/checker.php
r2946640
r2957322
369
369
$objVideo->updateMetaValue('last\_video\_meta\_check', time());
370
370
371
if( $meta\_data\['is\_live'\] ) {
371
if( is\_array($meta\_data) && !empty($meta\_data\['is\_live'\]) && $meta\_data\['is\_live'\] ) {
372
372
$objVideo->updateMetaValue( 'live', true );
373
373
} else {
…
…
375
375
}
376
376
377
if( $meta\_data\['duration'\] ) {
377
if( is\_array($meta\_data) && !empty($meta\_data\['duration'\]) && $meta\_data\['duration'\] ) {
378
378
$objVideo->updateMetaValue( 'duration', $meta\_data\['duration'\] );
379
379
}
380
380
381
if( $meta\_data\['error'\] ) {
381
if( is\_array($meta\_data) && !empty($meta\_data\['error'\]) && $meta\_data\['error'\] ) {
382
382
$objVideo->updateMetaValue( 'error', $meta\_data\['error'\] );
383
383
fv-wordpress-flowplayer/trunk/models/custom-videos.php
r2904314
r2957322
41
41
42
42
if( is\_admin() ) {
43
global $fv\_fp;
44
43
if( $this->have\_videos() ) {
45
44
global $FV\_Player\_Pro;
…
…
65
64
$html .= $this->get\_html( $args );
66
65
67
if( !is\_admin() ) {
68
$html .= wp\_nonce\_field( 'fv-player-custom-videos-'.$this->meta.'-'.get\_current\_user\_id(), 'fv-player-custom-videos-'.$this->meta.'-'.get\_current\_user\_id(), true, false );
69
}
70
71
if( !is\_admin() && !$args\['no\_form'\] ) {
66
$html .= wp\_nonce\_field( 'fv-player-custom-videos-'.$this->meta.'-'.get\_current\_user\_id(), 'fv-player-custom-videos-'.$this->meta.'-'.get\_current\_user\_id(), true, false );
67
68
if( !is\_admin() && !$args\['no\_form'\] ) {
72
69
$html .= "<input type='hidden' name='action' value='fv-player-custom-videos-save' />";
73
$html .= "<input type='submit' value='Save Videos' />";
70
$html .= "<input type='submit' value='Save Videos' />";
74
71
$html .= "</form>";
75
72
}
…
…
88
85
$args = !empty($post) && !empty($FV\_Player\_Custom\_Videos\_Master->aMetaBoxes\[$post->post\_type\]) ? $FV\_Player\_Custom\_Videos\_Master->aMetaBoxes\[$post->post\_type\]\[$this->meta\] : $defaults;
89
86
87
if( $video ) {
88
$video = wp\_kses( $video, 'post' );
89
}
90
90
91
// exp: what matters here is .fv-player-editor-field and .fv-player-editor-button wrapped in .fv-player-editor-wrapper and .fv-player-editor-preview
91
92
if( $edit ) {
…
…
94
95
$preview = false;
95
96
$before = 0;
97
96
98
if( $video ) {
99
global $fv\_fp;
100
97
101
$preview = do\_shortcode( str\_replace( '\[fvplayer ', '\[fvplayer autoplay="false" ', $video ) );
98
global $fv\_fp;
102
99
103
if( $fv\_fp->current\_player() ) {
100
104
$before = count($fv\_fp->current\_player()->getVideos());
…
…
120
124
</div>";
121
125
} else {
122
$html = do\_shortcode($video);
126
$html = do\_shortcode($video);
123
127
}
124
128
return $html;
…
…
146
150
} else if( $args\['edit'\] ) {
147
151
$html .= '<'.$args\['wrapper'\].' class="fv-player-custom-video">';
148
$html .= $this->get\_html\_part(false, true);
149
$html .= '<div style="clear: both"></div>'."\\n";
150
$html .= '</'.$args\['wrapper'\].'>';
152
$html .= $this->get\_html\_part(false, true);
153
$html .= '<div style="clear: both"></div>'."\\n";
154
$html .= '</'.$args\['wrapper'\].'>';
151
155
}
152
156
…
…
159
163
public function get_videos() {
160
164
if( $this->type == 'user' ) {
161
$aMeta = get\_user\_meta( $this->id, $this->meta );
165
$aMeta = get\_user\_meta( $this->id, $this->meta );
162
166
} else if( $this->type == 'post' ) {
163
167
$aMeta = get\_post\_meta( $this->id, $this->meta );
…
…
324
328
325
329
function save() {
326
327
330
if( !isset($\_POST\['fv\_player\_videos'\]) || !isset($\_POST\['fv-player-custom-videos-entity-type'\]) || !isset($\_POST\['fv-player-custom-videos-entity-id'\]) ) {
328
331
return;
329
332
}
330
331
332
333
// todo: permission check!
333
334
334
foreach( $\_POST\['fv\_player\_videos'\] AS $meta => $videos ) {
335
$meta = sanitize\_text\_field( $meta );
336
337
if( !wp\_verify\_nonce($\_POST\['fv-player-custom-videos-'.$meta.'-'.get\_current\_user\_id()\] ,'fv-player-custom-videos-'.$meta.'-'.get\_current\_user\_id() ) ) {
338
continue;
339
}
340
335
341
if( $\_POST\['fv-player-custom-videos-entity-type'\]\[$meta\] == 'user' ) {
336
342
delete\_user\_meta( $\_POST\['fv-player-custom-videos-entity-id'\]\[$meta\], $meta );
…
…
338
344
foreach( $videos AS $video ) {
339
345
if( strlen($video) == 0 ) continue;
340
346
347
// strip html tags to prevent XSS
348
$video = sanitize\_text\_field( $video );
349
341
350
add\_user\_meta( $\_POST\['fv-player-custom-videos-entity-id'\]\[$meta\], $meta, $video );
342
351
}
343
}
344
345
}
346
352
}
353
}
347
354
}
348
355
…
…
351
358
return;
352
359
}
353
354
// todo: permission check!
355
360
356
361
foreach( $\_POST\['fv\_player\_videos'\] AS $meta => $value ) {
362
$meta = sanitize\_text\_field( $meta );
363
364
if( !wp\_verify\_nonce($\_POST\['fv-player-custom-videos-'.$meta.'-'.get\_current\_user\_id()\] ,'fv-player-custom-videos-'.$meta.'-'.get\_current\_user\_id() ) ) {
365
continue;
366
}
367
357
368
if( $\_POST\['fv-player-custom-videos-entity-type'\]\[$meta\] == 'post' && $\_POST\['fv-player-custom-videos-entity-id'\]\[$meta\] == $post\_id ) {
358
369
delete\_post\_meta( $post\_id, $meta );
359
370
360
371
if( is\_array($value) && count($value) > 0 ) {
361
foreach( $value AS $k => $v ) {
372
foreach( $value AS $k => $v ) {
362
373
if( strlen($v) == 0 ) continue;
363
374
375
// strip html tags to prevent XSS
376
$v = sanitize\_text\_field( $v );
377
364
378
add\_post\_meta( $post\_id, $meta, $v );
365
379
}
fv-wordpress-flowplayer/trunk/models/db-player-meta.php
r2904314
r2957322
85
85
return self::$db\_table\_name;
86
86
}
87
87
88
/**
89
* Returns name of the video DB table.
90
*
91
* @return mixed The name of the video DB table.
92
*/
93
public static function get_db_table_name() {
94
if ( !self::$db\_table\_name ) {
95
self::init\_db\_name();
96
}
97
98
return self::$db\_table\_name;
99
}
100
88
101
/**
89
102
* Checks for DB tables existence and creates it as necessary.
fv-wordpress-flowplayer/trunk/models/flowplayer.php
r2946640
r2957322
893
893
894
894
$temp\_media = $this->get\_video\_src( $v\['src'\], array( 'dynamic' => true ) );
895
896
// Encode components of the URL path
897
$url\_path = wp\_parse\_url( $temp\_media, PHP\_URL\_PATH );
898
$temp\_media = str\_replace( $url\_path, implode( '/', array\_map( 'urlencode', explode( '/', $url\_path ) ) ), $temp\_media );
899
900
895
if( isset($FV\_Player\_Pro) && $FV\_Player\_Pro ) {
901
896
if($FV\_Player\_Pro->is\_vimeo($temp\_media) || method\_exists($FV\_Player\_Pro, 'is\_vimeo\_event') && $FV\_Player\_Pro->is\_vimeo\_event($temp\_media) || $FV\_Player\_Pro->is\_youtube($temp\_media)) {
…
…
1526
1521
1527
1522
function css_option() {
1528
return 'css\_writeout-'.sanitize\_title(home\_url());
1523
global $fv\_wp\_flowplayer\_ver;
1524
return 'css\_writeout-'.sanitize\_title(home\_url()) . '-' . $fv\_wp\_flowplayer\_ver;
1529
1525
}
1530
1526
…
…
1734
1730
1735
1731
$url\_components = parse\_url($resource);
1736
1737
$iAWSVersion = $fv\_fp->\_get\_option( array( 'amazon\_region', $amazon\_key ) ) ? 4 : 2;
1738
1739
if( $iAWSVersion == 4 ) {
1740
$url\_components\['path'\] = str\_replace( array('%20','+'), ' ', $url\_components\['path'\]);
1741
}
1742
1743
$url\_components\['path'\] = rawurlencode($url\_components\['path'\]);
1732
1733
// decode the path, as it might come partially URL encoded already
1734
$url\_components\['path'\] = urldecode( $url\_components\['path'\] );
1735
1736
// URL encode the decoded path
1737
$url\_components\['path'\] = rawurlencode( $url\_components\['path'\] );
1738
1739
// Restore the directory separators
1744
1740
$url\_components\['path'\] = str\_replace('%2F', '/', $url\_components\['path'\]);
1745
$url\_components\['path'\] = str\_replace('%2B', '+', $url\_components\['path'\]);
1746
$url\_components\['path'\] = str\_replace('%2523', '%23', $url\_components\['path'\]);
1747
$url\_components\['path'\] = str\_replace('%252B', '%2B', $url\_components\['path'\]);
1748
$url\_components\['path'\] = str\_replace('%2527', '%27', $url\_components\['path'\]);
1749
1750
if( $iAWSVersion == 4 ) {
1751
$sXAMZDate = gmdate('Ymd\\THis\\Z');
1752
$sDate = gmdate('Ymd');
1753
$sCredentialScope = $sDate."/".$fv\_fp->\_get\_option( array('amazon\_region', $amazon\_key ) )."/s3/aws4\_request"; // todo: variable
1754
$sSignedHeaders = "host";
1755
$sXAMZCredential = urlencode( $fv\_fp->\_get\_option( array('amazon\_key', $amazon\_key ) ).'/'.$sCredentialScope);
1756
1757
// 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
1758
$sCanonicalRequest = "GET\\n";
1759
$sCanonicalRequest .= $url\_components\['path'\]."\\n";
1760
$sCanonicalRequest .= "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=$sXAMZCredential&X-Amz-Date=$sXAMZDate&X-Amz-Expires=$time&X-Amz-SignedHeaders=$sSignedHeaders\\n";
1761
$sCanonicalRequest .= "host:".$url\_components\['host'\]."\\n";
1762
$sCanonicalRequest .= "\\n$sSignedHeaders\\n";
1763
$sCanonicalRequest .= "UNSIGNED-PAYLOAD";
1764
1765
// 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
1766
$sStringToSign = "AWS4-HMAC-SHA256\\n";
1767
$sStringToSign .= "$sXAMZDate\\n";
1768
$sStringToSign .= "$sCredentialScope\\n";
1769
$sStringToSign .= hash('sha256',$sCanonicalRequest);
1770
1771
// 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
1772
$sSignature = hash\_hmac('sha256', $sDate, "AWS4".$fv\_fp->\_get\_option( array('amazon\_secret', $amazon\_key) ), true );
1773
$sSignature = hash\_hmac('sha256', $fv\_fp->\_get\_option( array('amazon\_region', $amazon\_key) ), $sSignature, true ); // todo: variable
1774
$sSignature = hash\_hmac('sha256', 's3', $sSignature, true );
1775
$sSignature = hash\_hmac('sha256', 'aws4\_request', $sSignature, true );
1776
$sSignature = hash\_hmac('sha256', $sStringToSign, $sSignature );
1777
1778
// 4. http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
1779
$resource .= "?X-Amz-Algorithm=AWS4-HMAC-SHA256";
1780
$resource .= "&X-Amz-Credential=$sXAMZCredential";
1781
$resource .= "&X-Amz-Date=$sXAMZDate";
1782
$resource .= "&X-Amz-Expires=$time";
1783
$resource .= "&X-Amz-SignedHeaders=$sSignedHeaders";
1784
$resource .= "&X-Amz-Signature=".$sSignature;
1785
1786
} else {
1787
$expires = time() + $time;
1788
1789
if( strpos( $url\_components\['path'\], $fv\_fp->\_get\_option( array('amazon\_bucket', $amazon\_key) ) ) === false ) {
1790
$url\_components\['path'\] = '/'.$fv\_fp->\_get\_option( array('amazon\_bucket', $amazon\_key) ).$url\_components\['path'\];
1791
}
1792
1793
do {
1794
$expires++;
1795
$stringToSign = "GET\\n\\n\\n$expires\\n{$url\_components\['path'\]}";
1796
1797
$signature = utf8\_encode($stringToSign);
1798
1799
$signature = hash\_hmac('sha1', $signature, $fv\_fp->\_get\_option( array('amazon\_secret', $amazon\_key ) ), true);
1800
$signature = base64\_encode($signature);
1801
1802
$signature = urlencode($signature);
1803
} while( stripos($signature,'%2B') !== false );
1804
1805
$resource .= '?AWSAccessKeyId='.$fv\_fp->\_get\_option( array('amazon\_key', $amazon\_key) ).'&Expires='.$expires.'&Signature='.$signature;
1806
1807
}
1808
1741
1742
$sXAMZDate = gmdate('Ymd\\THis\\Z');
1743
$sDate = gmdate('Ymd');
1744
$sCredentialScope = $sDate."/".$fv\_fp->\_get\_option( array('amazon\_region', $amazon\_key ) )."/s3/aws4\_request";
1745
$sSignedHeaders = "host";
1746
$sXAMZCredential = urlencode( $fv\_fp->\_get\_option( array('amazon\_key', $amazon\_key ) ).'/'.$sCredentialScope);
1747
1748
// 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
1749
$sCanonicalRequest = "GET\\n";
1750
$sCanonicalRequest .= $url\_components\['path'\]."\\n";
1751
$sCanonicalRequest .= "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=$sXAMZCredential&X-Amz-Date=$sXAMZDate&X-Amz-Expires=$time&X-Amz-SignedHeaders=$sSignedHeaders\\n";
1752
$sCanonicalRequest .= "host:".$url\_components\['host'\]."\\n";
1753
$sCanonicalRequest .= "\\n$sSignedHeaders\\n";
1754
$sCanonicalRequest .= "UNSIGNED-PAYLOAD";
1755
1756
// 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
1757
$sStringToSign = "AWS4-HMAC-SHA256\\n";
1758
$sStringToSign .= "$sXAMZDate\\n";
1759
$sStringToSign .= "$sCredentialScope\\n";
1760
$sStringToSign .= hash('sha256',$sCanonicalRequest);
1761
1762
// 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
1763
$sSignature = hash\_hmac('sha256', $sDate, "AWS4".$fv\_fp->\_get\_option( array('amazon\_secret', $amazon\_key) ), true );
1764
$sSignature = hash\_hmac('sha256', $fv\_fp->\_get\_option( array('amazon\_region', $amazon\_key) ), $sSignature, true );
1765
$sSignature = hash\_hmac('sha256', 's3', $sSignature, true );
1766
$sSignature = hash\_hmac('sha256', 'aws4\_request', $sSignature, true );
1767
$sSignature = hash\_hmac('sha256', $sStringToSign, $sSignature );
1768
1769
// 4. http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
1770
$resource .= "?X-Amz-Algorithm=AWS4-HMAC-SHA256";
1771
$resource .= "&X-Amz-Credential=$sXAMZCredential";
1772
$resource .= "&X-Amz-Date=$sXAMZDate";
1773
$resource .= "&X-Amz-Expires=$time";
1774
$resource .= "&X-Amz-SignedHeaders=$sSignedHeaders";
1775
$resource .= "&X-Amz-Signature=".$sSignature;
1776
1809
1777
$media = $resource;
1810
1778
fv-wordpress-flowplayer/trunk/models/stats.php
r2904314
r2957322
59
59
}
60
60
61
function get_table_name() {
61
public static function get_table_name() {
62
62
global $wpdb;
63
63
return $wpdb->prefix . 'fv\_player\_stats';
fv-wordpress-flowplayer/trunk/readme.txt
r2954757
r2957322
360
360
== Changelog ==
361
361
362
= 7.5.39.7212 - 2023/08/23 =
363
364
* Security - prevent XSS when “Enable profile videos” is on
365
* Allow DB tables to be fixed with WP_ALLOW_REPAIR
366
* Bugfix - Amazon S3 - fix when using commas in the URLs
367
* Bugfix - Video Checker - fix checking of dynamic URLs (Amazon S3, or FV Player Pro CDNs like Bunny CDN)
368
362
369
= 7.5.37.7212 - 2023/08/02 =
363
370