CVE-2024-1109: Missing Authorization (CWE-862)
value: The Podlove Podcast Publisher plugin for WordPress is vulnerable to unauthorized access of data due to a missing capability check on the init_download() and init() functions in all versions up to, and including, 4.0.11. This makes it possible for unauthenticated attackers to export the plugin's tracking data and podcast information.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 5.3
Published: 2024-02-07T11:15Z
Repository: https://github.com/podlove/podlove-publisher
Language: PHP
Files changed: lib/modules/import_export/export/podcast_exporter.php, lib/modules/import_export/export/tracking_exporter.php, lib/modules/import_export/import_export.php, readme.txt
Commit date: 2024-02-06 08:42:15 +0100
Commit message: fix: add missing capability check and nonce validation to exporter functions

Diff: diff --git a/lib/modules/import_export/export/podcast_exporter.php b/lib/modules/import_export/export/podcast_exporter.php
index 1eefca8e8..a994582f3 100644
--- a/lib/modules/import_export/export/podcast_exporter.php
+++ b/lib/modules/import_export/export/podcast_exporter.php
@@ -28,10 +28,18 @@ class PodcastExporter
         if (!is_admin()) {
             return;
         }
 
         if (isset($_GET['podlove_export']) && $_GET['podlove_export']) {
+            if (!current_user_can('administrator')) {
+                return;
+            }
+
+            if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_export')) {
+                return;
+            }
+
             $exporter = new \Podlove\Modules\ImportExport\Export\PodcastExporter();
             $exporter->download();
             exit;
         }
     }
diff --git a/lib/modules/import_export/export/tracking_exporter.php b/lib/modules/import_export/export/tracking_exporter.php
index 23de6eaa7..231cf5e30 100644
--- a/lib/modules/import_export/export/tracking_exporter.php
+++ b/lib/modules/import_export/export/tracking_exporter.php
@@ -17,10 +17,18 @@ class TrackingExporter
         if (!is_admin()) {
             return;
         }
 
         if (isset($_GET['podlove_export_tracking']) && $_GET['podlove_export_tracking']) {
+            if (!current_user_can('administrator')) {
+                exit;
+            }
+
+            if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_export_tracking_download')) {
+                exit;
+            }
+
             delete_transient('podlove_tracking_export_finished');
 
             header('Content-Type: application/octet-stream');
             header('Content-Description: File Transfer');
             header('Content-Disposition: attachment; filename='.TrackingExporter::getDownloadFileName());
@@ -41,13 +49,21 @@ class TrackingExporter
 
     public static function export_tracking()
     {
         global $wpdb;
 
+        if (!current_user_can('administrator')) {
+            exit;
+        }
+
+        if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_export_tracking')) {
+            exit;
+        }
+
         // only one export at a time
         if (get_option('podlove_tracking_export_all') !== false) {
-            return;
+            exit;
         }
 
         update_option('podlove_tracking_export_all', $wpdb->get_var('SELECT COUNT(*) FROM '.\Podlove\Model\DownloadIntent::table_name()));
         update_option('podlove_tracking_export_progress', 0);
 
diff --git a/lib/modules/import_export/import_export.php b/lib/modules/import_export/import_export.php
index 7e8a81f3b..d4834d3db 100644
--- a/lib/modules/import_export/import_export.php
+++ b/lib/modules/import_export/import_export.php
@@ -121,11 +121,11 @@ class Import_Export extends \Podlove\Modules\Base
 			<li><?php echo __('Import this file to your new WordPress instance.', 'podlove-podcasting-plugin-for-wordpress'); ?></li>
 			<li><?php echo __('Use the button below to export the podcast data file.', 'podlove-podcasting-plugin-for-wordpress'); ?></li>
 			<li><?php echo __('In your new WordPress instance, import that file.', 'podlove-podcasting-plugin-for-wordpress'); ?></li>
 		</ol>
 
-		<a href="?podlove_export=1" class="button"><?php echo __('Export Podcast Data', 'podlove-podcasting-plugin-for-wordpress'); ?></a>
+		<a href="?podlove_export=1&_podlove_nonce=<?php echo wp_create_nonce('podlove_export'); ?>" class="button"><?php echo __('Export Podcast Data', 'podlove-podcasting-plugin-for-wordpress'); ?></a>
 		<?php
     }
 
     public function tools_tracking_export()
     {
@@ -166,16 +166,16 @@ class Import_Export extends \Podlove\Modules\Base
 							$("#podlove_tracking_export").attr('disabled', 'disabled');
 							$("#podlove_tracking_export_status").html((Math.round(1000.0 * (result.progress / result.all))/10.0) + "%");
 							$("#podlove_tracking_export_status_wrapper").show();
 
 							timeoutID = window.setTimeout(podlove_check_export_status, 1000);
-						} 
+						}
 
 						if (result.finished) {
 							$("#podlove_tracking_export").attr('disabled', false);
 							$("#podlove_tracking_export_status_wrapper").hide();
-							window.location = window.location + "&podlove_export_tracking=1";
+							window.location = window.location + "&podlove_export_tracking=1&_podlove_nonce=<?php echo wp_create_nonce('podlove_export_tracking_download'); ?>";
 						}
 					}
 				});
 
 			};
@@ -185,24 +185,23 @@ class Import_Export extends \Podlove\Modules\Base
 				$("#podlove_tracking_export").attr('disabled', 'disabled');
 				$("#podlove_tracking_export_status_wrapper").show();
 
 				$.ajax({
 					url: ajaxurl,
-					data: {action: 'podlove-export-tracking'},
-					dataType: 'json',
-					success: function(result) {
+					data: {action: 'podlove-export-tracking', _podlove_nonce: '<?php echo wp_create_nonce('podlove_export_tracking'); ?>'},
+					dataType: 'json'
+				}).done(function(result) {
 						console.log("tracking export finished");
-					}
+				    window.setTimeout(podlove_check_export_status, 2000);
 				});
 
-				window.setTimeout(podlove_check_export_status, 2000);
 			});
 
 			// start immediately, in case the user refreshes the page
 			podlove_check_export_status();
 		}(jQuery));
-		</script>		
+		</script>
 		<?php
     }
 
     public function tools_podcast_import()
     {
@@ -211,14 +210,14 @@ class Import_Export extends \Podlove\Modules\Base
 			<?php echo __('Use this import on <strong>fresh installs only</strong>! Otherwise you may lose data. In any case, you should have backups.', 'podlove-podcasting-plugin-for-wordpress'); ?>
 		</p>
 
 		<form method="POST" enctype="multipart/form-data">
 			(<span><?php echo self::get_maximum_upload_size_text(); ?></span>)
-			<input type="file" name="podlove_import"/> 
+			<input type="file" name="podlove_import"/>
 			<input type="submit" value="<?php echo __('Import Podcast Data', 'podlove-podcasting-plugin-for-wordpress'); ?>" class="button" />
 			<?php wp_nonce_field('podlove_import', '_podlove_nonce'); ?>
-		</form>		
+		</form>
 		<?php
     }
 
     public function tools_tracking_import()
     {
@@ -226,11 +225,11 @@ class Import_Export extends \Podlove\Modules\Base
 		<form method="POST" enctype="multipart/form-data">
 			(<span><?php echo self::get_maximum_upload_size_text(); ?></span>)
 			<input type="file" name="podlove_import_tracking"/>
 			<input type="submit" value="<?php echo __('Import Tracking Data', 'podlove-podcasting-plugin-for-wordpress'); ?>" class="button" />
 			<?php wp_nonce_field('podlove_import_tracking', '_podlove_nonce'); ?>
-		</form>		
+		</form>
 		<?php
     }
 
     public static function get_maximum_upload_size_text()
     {
diff --git a/readme.txt b/readme.txt
index 72377c86f..4a183cb3c 100644
--- a/readme.txt
+++ b/readme.txt
@@ -110,10 +110,11 @@ This product includes GeoLite2 data created by MaxMind, available from http://ww
 
 **Security**
 
 - fix SSRF vulnerability in Slacknotes module
 - add missing capability check and nonce validation to importer functions
+- add missing capability check and nonce validation to exporter functions
 
 = 4.0.11 =
 
 - new: show admin notice when a database migration fails
 - fix bug where tracking data could be lost by disabling a media file checkbox

Vulnerable code:             return;
		<a href="?podlove_export=1" class="button"><?php echo __('Export Podcast Data', 'podlove-podcasting-plugin-for-wordpress'); ?></a>
						} 
							window.location = window.location + "&podlove_export_tracking=1";
					data: {action: 'podlove-export-tracking'},
					dataType: 'json',
					success: function(result) {
					}
				window.setTimeout(podlove_check_export_status, 2000);
		</script>		
			<input type="file" name="podlove_import"/> 
		</form>		
		</form>		
Fixed code:             if (!current_user_can('administrator')) {
                return;
            }

            if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_export')) {
                return;
            }

            if (!current_user_can('administrator')) {
                exit;
            }

            if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_export_tracking_download')) {
                exit;
            }

        if (!current_user_can('administrator')) {
            exit;
        }

        if (!wp_verify_nonce($_REQUEST['_podlove_nonce'], 'podlove_export_tracking')) {
            exit;
        }

            exit;
		<a href="?podlove_export=1&_podlove_nonce=<?php echo wp_create_nonce('podlove_export'); ?>" class="button"><?php echo __('Export Podcast Data', 'podlove-podcasting-plugin-for-wordpress'); ?></a>
						}
							window.location = window.location + "&podlove_export_tracking=1&_podlove_nonce=<?php echo wp_create_nonce('podlove_export_tracking_download'); ?>";
					data: {action: 'podlove-export-tracking', _podlove_nonce: '<?php echo wp_create_nonce('podlove_export_tracking'); ?>'},
					dataType: 'json'
				}).done(function(result) {
				    window.setTimeout(podlove_check_export_status, 2000);
		</script>
			<input type="file" name="podlove_import"/>
		</form>
		</form>
- add missing capability check and nonce validation to exporter functions