Privacy: Add cron to delete expired export files to protect privacy.

The primary means of protecting the files is the CSPRN appended to the filename, but there is no reason to keep the files after the data subject has downloaded them, so deleting them provides an additional layer of protection. Previously this was done from `wp_privacy_generate_personal_data_export_file()`, but that does not guarantee that it will be run regularly, and on smaller sites that could result in export files being exposed for much longer than necessary.

`wp_privacy_delete_old_export_files()` was moved to a front end file, so that it can be called from `cron.php`.

This introduces the `wp_privacy_export_expiration` filter, which allows plugins to customize how long the exports are kept before being deleted.

`index.html` was added to the `$exclusions` parameter of `list_files()` to make sure that it isn't deleted. If it were, then poorly-configured servers would allow the directory to be traversed, exposing all of the exported files.

Props iandunn, desrosj.
See #43546.

Built from https://develop.svn.wordpress.org/trunk@43046


git-svn-id: http://core.svn.wordpress.org/trunk@42875 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
iandunn 2018-04-30 20:09:23 +00:00
parent 5b4aa26436
commit 23bf0c7535
4 changed files with 61 additions and 26 deletions

View File

@ -1999,9 +1999,6 @@ function wp_privacy_generate_personal_data_export_group_html( $group_data ) {
* @param int $request_id The export request ID. * @param int $request_id The export request ID.
*/ */
function wp_privacy_generate_personal_data_export_file( $request_id ) { function wp_privacy_generate_personal_data_export_file( $request_id ) {
// Maybe make this a cron job instead.
wp_privacy_delete_old_export_files();
if ( ! class_exists( 'ZipArchive' ) ) { if ( ! class_exists( 'ZipArchive' ) ) {
wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) ); wp_send_json_error( __( 'Unable to generate export file. ZipArchive not available.' ) );
} }
@ -2162,13 +2159,18 @@ function wp_privacy_send_personal_data_export_email( $request_id ) {
return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) ); return new WP_Error( 'invalid', __( 'Invalid request ID when sending personal data export email.' ) );
} }
/* translators: Do not translate LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */ /** This filter is documented in wp-admin/includes/file.php */
$expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
$expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );
/* translators: Do not translate EXPIRATION, LINK, EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_text = __( $email_text = __(
'Howdy, 'Howdy,
Your request for an export of personal data has been completed. You may Your request for an export of personal data has been completed. You may
download your personal data by clicking on the link below. This link is download your personal data by clicking on the link below. For privacy
good for the next 3 days. and security, we will automatically delete the file on ###EXPIRATION###,
so please download it before then.
###LINK### ###LINK###
@ -2183,6 +2185,7 @@ All at ###SITENAME###
* Filters the text of the email sent with a personal data export file. * Filters the text of the email sent with a personal data export file.
* *
* The following strings have a special meaning and will get replaced dynamically: * The following strings have a special meaning and will get replaced dynamically:
* ###EXPIRATION### The date when the URL will be automatically deleted.
* ###LINK### URL of the personal data export file for the user. * ###LINK### URL of the personal data export file for the user.
* ###EMAIL### The email we are sending to. * ###EMAIL### The email we are sending to.
* ###SITENAME### The name of the site. * ###SITENAME### The name of the site.
@ -2200,6 +2203,7 @@ All at ###SITENAME###
$site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ); $site_name = is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
$site_url = network_home_url(); $site_url = network_home_url();
$content = str_replace( '###EXPIRATION###', $expiration_date, $content );
$content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content ); $content = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $content );
$content = str_replace( '###EMAIL###', $email_address, $content ); $content = str_replace( '###EMAIL###', $email_address, $content );
$content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content ); $content = str_replace( '###SITENAME###', wp_specialchars_decode( $site_name, ENT_QUOTES ), $content );
@ -2344,22 +2348,3 @@ function wp_privacy_process_personal_data_export_page( $response, $exporter_inde
return $response; return $response;
} }
/**
* Cleans up export files older than three days old.
*
* @since 4.9.6
*/
function wp_privacy_delete_old_export_files() {
$upload_dir = wp_upload_dir();
$exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' );
$export_files = list_files( $exports_dir );
foreach( (array) $export_files as $export_file ) {
$file_age_in_seconds = time() - filemtime( $export_file );
if ( 3 * DAY_IN_SECONDS < $file_age_in_seconds ) {
@unlink( $export_file );
}
}
}

View File

@ -352,6 +352,8 @@ add_action( 'user_request_action_confirmed', '_wp_privacy_account_request_confir
add_filter( 'user_request_action_confirmed_message', '_wp_privacy_account_request_confirmed_message', 10, 2 ); add_filter( 'user_request_action_confirmed_message', '_wp_privacy_account_request_confirmed_message', 10, 2 );
add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter' ); add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter' );
add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser' ); add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser' );
add_action( 'init', 'wp_schedule_delete_old_privacy_export_files' );
add_action( 'wp_privacy_delete_old_export_files', 'wp_privacy_delete_old_export_files' );
// Cron tasks // Cron tasks
add_action( 'wp_scheduled_delete', 'wp_scheduled_delete' ); add_action( 'wp_scheduled_delete', 'wp_scheduled_delete' );

View File

@ -6257,3 +6257,51 @@ function wp_privacy_anonymize_data( $type, $data = '' ) {
function _wp_privacy_active_plugins_change() { function _wp_privacy_active_plugins_change() {
update_option( '_wp_privacy_text_change_check', 'check' ); update_option( '_wp_privacy_text_change_check', 'check' );
} }
/**
* Schedule a `WP_Cron` job to delete expired export files.
*
* @since 4.9.6
*/
function wp_schedule_delete_old_privacy_export_files() {
if ( ! wp_next_scheduled( 'wp_privacy_delete_old_export_files' ) ) {
wp_schedule_event( time(), 'hourly', 'wp_privacy_delete_old_export_files' );
}
}
/**
* Cleans up export files older than three days old.
*
* The export files are stored in `wp-content/uploads`, and are therefore publicly
* accessible. A CSPRN is appended to the filename to mitigate the risk of an
* unauthorized person downloading the file, but it is still possible. Deleting
* the file after the data subject has had a chance to delete it adds an additional
* layer of protection.
*
* @since 4.9.6
*/
function wp_privacy_delete_old_export_files() {
$upload_dir = wp_upload_dir();
$exports_dir = trailingslashit( $upload_dir['basedir'] . '/exports' );
$export_files = list_files( $exports_dir, 100, array( 'index.html' ) );
/**
* Filters the lifetime, in seconds, of a personal data export file.
*
* By default, the lifetime is 3 days. Once the file reaches that age, it will automatically
* be deleted by a cron job.
*
* @since 4.9.6
*
* @param int $expiration The expiration age of the export, in seconds.
*/
$expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
foreach ( (array) $export_files as $export_file ) {
$file_age_in_seconds = time() - filemtime( $export_file );
if ( $expiration < $file_age_in_seconds ) {
unlink( $export_file );
}
}
}

View File

@ -4,7 +4,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.0-alpha-43045'; $wp_version = '5.0-alpha-43046';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.