Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/envira-gallery-lite/envira-gallery-lite.php
+++ b/envira-gallery-lite/envira-gallery-lite.php
@@ -5,7 +5,7 @@
* Description: Envira Gallery is a fast, easy and powerful gallery builder with lightbox, masonry and grid layouts, albums, videos, and responsive displays and more
* Author: Envira Gallery Team
* Author URI: http://enviragallery.com
- * Version: 1.12.4
+ * Version: 1.12.5
* Requires at least: 5.5
* Requires PHP: 7.0
* Text Domain: envira-gallery-lite
@@ -59,7 +59,7 @@
*
* @var string
*/
- public $version = '1.12.4';
+ public $version = '1.12.5';
/**
--- a/envira-gallery-lite/includes/admin/Envira_Lite_Support.php
+++ b/envira-gallery-lite/includes/admin/Envira_Lite_Support.php
@@ -73,14 +73,23 @@
return;
}
- $valid_request = isset( $_POST['action'], $_POST['envira_nonce'] );
- $valid_nonce = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['envira_nonce'] ) ), $this->nonce_action );
+ // Explicit capability check as defense-in-depth. Use the same filtered capability
+ // as the submenu registration so customized access remains consistent here too.
+ $capability = apply_filters( 'envira_gallery_menu_cap_support', 'manage_options' );
+ if ( ! current_user_can( $capability ) ) {
+ return;
+ }
+
+ if ( ! isset( $_POST['envira_nonce'] ) ) {
+ return;
+ }
- if ( ! $valid_request || ! $valid_nonce ) {
+ if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['envira_nonce'] ) ), $this->nonce_action ) ) {
return;
}
- $gallery_id = isset( $_POST['gallery_id'] ) ? intval( $_POST['gallery_id'] ) : null;
- $action = sanitize_text_field( wp_unslash( $_POST['action'] ) );
+
+ $action = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : '';
+ $gallery_id = isset( $_POST['gallery_id'] ) ? intval( wp_unslash( $_POST['gallery_id'] ) ) : null;
switch ( $action ) {
case 'toggle-debug': // General Tab.
--- a/envira-gallery-lite/includes/admin/addons.php
+++ b/envira-gallery-lite/includes/admin/addons.php
@@ -430,8 +430,7 @@
* @return bool True if being refreshed, false otherwise.
*/
public function is_refreshing_addons() {
-
- return isset( $_POST['envira-gallery-refresh-addons'] ); // @codingStandardsIgnoreLine
+ return $this->refresh_addons_action();
}
/**
@@ -443,7 +442,7 @@
*/
public function refresh_addons_action() {
- return isset( $_POST['envira-gallery-refresh-addons'] ) && wp_verify_nonce( sanitize_key( $_POST['envira-gallery-refresh-addons'] ), 'envira-gallery-refresh-addons' );
+ return isset( $_POST['envira-gallery-refresh-addons'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['envira-gallery-refresh-addons'] ) ), 'envira-gallery-refresh-addons' );
}
/**
--- a/envira-gallery-lite/includes/admin/ajax.php
+++ b/envira-gallery-lite/includes/admin/ajax.php
@@ -12,6 +12,51 @@
exit; // Exit if accessed directly.
}
+/**
+ * Validates that a plugin download URL is safe to pass to Plugin_Upgrader.
+ *
+ * Why no domain allowlist:
+ * The caller already holds the install_plugins capability (admin-level), which
+ * means WordPress itself will let them install from any URL via the native UI or
+ * WP-CLI. Restricting domains here adds friction without closing any real attack
+ * vector that the capability check doesn't already close.
+ *
+ * Internal-IP / SSRF attacks are already blocked by WP_HTTP::block_request(),
+ * which rejects private RFC-1918 ranges, loopback, and cloud-metadata IPs before
+ * the HTTP request is ever made.
+ *
+ * What we DO enforce:
+ * - Must use HTTPS (prevents credential interception on the wire).
+ * - Must end with .zip (Plugin_Upgrader only handles ZIP archives anyway).
+ * - Must be a well-formed URL with a non-empty host.
+ *
+ * @since 1.12.6
+ *
+ * @param string $url The download URL supplied by the caller.
+ * @return bool True when the URL passes all checks, false otherwise.
+ */
+function envira_gallery_is_valid_plugin_download_url( string $url ): bool {
+ $parsed = wp_parse_url( $url );
+
+ // Must have a scheme and a host.
+ if ( empty( $parsed['scheme'] ) || empty( $parsed['host'] ) ) {
+ return false;
+ }
+
+ // Enforce HTTPS — plain HTTP exposes the ZIP to interception/tampering.
+ if ( 'https' !== strtolower( $parsed['scheme'] ) ) {
+ return false;
+ }
+
+ // Plugin archives are always ZIP files; anything else is unexpected.
+ $path = isset( $parsed['path'] ) ? strtolower( $parsed['path'] ) : '';
+ if ( ! str_ends_with( $path, '.zip' ) ) {
+ return false;
+ }
+
+ return true;
+}
+
add_action( 'wp_ajax_envira_gallery_change_type', 'envira_gallery_ajax_change_type' );
/**
* Changes the type of gallery to the user selection.
@@ -19,19 +64,27 @@
* @since 1.0.0
*/
function envira_gallery_ajax_change_type() {
-
// Run a security check first.
- check_admin_referer( 'envira-gallery-change-type', 'nonce' );
+ check_ajax_referer( 'envira-gallery-change-type', 'nonce' );
// Prepare variables.
$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : null;
-
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
}
$post = get_post( $post_id );
+ // Sanitize at ingestion; strips tags and control characters.
$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : null;
+ // Validate against registered types; prevents arbitrary hook names and reflected values.
+ $instance = Envira_Gallery_Metaboxes::get_instance();
+ // Keys are the type slugs ('default', addon slugs, etc.).
+ $valid_types = array_keys( $instance->get_envira_types( $post ) );
+ // Strict comparison; prevents type-juggling bypass.
+ if ( ! in_array( $type, $valid_types, true ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid gallery type.', 'envira-gallery-lite' ) ] );
+ }
+
// Retrieve the data for the type selected.
ob_start();
$instance = Envira_Gallery_Metaboxes::get_instance();
@@ -57,16 +110,30 @@
function envira_gallery_ajax_set_user_setting() {
// Run a security check first.
- check_admin_referer( 'envira-gallery-set-user-setting', 'nonce' );
+ check_ajax_referer( 'envira-gallery-set-user-setting', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
}
+ // Allowlist map: only known settings and their permitted values are accepted.
+ // Prevents arbitrary key/value injection into user meta via set_user_setting().
+ $allowed_settings = [
+ // Only setting used by this plugin.
+ 'envira_gallery_image_view' => [ 'grid', 'list' ],
+ ];
+
// Prepare variables.
- $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
+ // Sanitize key at ingestion.
+ $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
+ // Sanitize value at ingestion.
$value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( $_POST['value'] ) ) : '';
+ // Reject unknown keys or invalid values — strict comparison prevents type-juggling bypass.
+ if ( ! array_key_exists( $name, $allowed_settings ) || ! in_array( $value, $allowed_settings[ $name ], true ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid setting name or value.', 'envira-gallery-lite' ) ] );
+ }
+
// Set user setting.
set_user_setting( $name, $value );
@@ -84,7 +151,7 @@
function envira_gallery_ajax_load_image() {
// Run a security check first.
- check_admin_referer( 'envira-gallery-load-image', 'nonce' );
+ check_ajax_referer( 'envira-gallery-load-image', 'nonce' );
// Prepare variables.
$id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : null;
@@ -94,6 +161,14 @@
wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
}
+ $attachment = get_post( $id );
+ if ( ! $attachment || ( 'attachment' !== $attachment->post_type ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid attachment.', 'envira-gallery-lite' ) ] );
+ }
+ if ( (int) $attachment->post_author !== get_current_user_id() && ! current_user_can( 'edit_others_posts' ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'You do not have permission to use this attachment.', 'envira-gallery-lite' ) ] );
+ }
+
// Set post meta to show that this image is attached to one or more Envira galleries.
$has_gallery = get_post_meta( $id, '_eg_has_gallery', true );
if ( empty( $has_gallery ) ) {
@@ -152,7 +227,7 @@
function envira_gallery_ajax_insert_images() {
// Run a security check first.
- check_admin_referer( 'envira-gallery-insert-images', 'nonce' );
+ check_ajax_referer( 'envira-gallery-insert-images', 'nonce' );
// Get the Envira Gallery ID.
$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : null;
@@ -169,7 +244,22 @@
$images = [];
if ( isset( $_POST['images'] ) ) {
- $images = json_decode( sanitize_text_field( wp_unslash( $_POST['images'] ) ), true );
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON blob is decoded, then every field inside the loop below is individually sanitized (absint/esc_url_raw/sanitize_text_field/wp_kses_post) before use.
+ $images = json_decode( wp_unslash( $_POST['images'] ), true );
+ if ( ! is_array( $images ) ) {
+ $images = [];
+ }
+ foreach ( $images as $i => $image ) {
+ $images[ $i ] = [
+ 'id' => isset( $image['id'] ) ? absint( $image['id'] ) : 0,
+ 'src' => isset( $image['src'] ) ? esc_url_raw( $image['src'] ) : '',
+ 'title' => isset( $image['title'] ) ? sanitize_text_field( $image['title'] ) : '',
+ 'alt' => isset( $image['alt'] ) ? sanitize_text_field( $image['alt'] ) : '',
+ 'caption' => isset( $image['caption'] ) ? wp_kses_post( $image['caption'] ) : '',
+ 'link' => isset( $image['link'] ) ? esc_url_raw( $image['link'] ) : '',
+ 'url' => isset( $image['url'] ) ? esc_url_raw( $image['url'] ) : '',
+ ];
+ }
}
// Grab and update any gallery data if necessary.
@@ -197,6 +287,14 @@
continue;
}
+ $att = get_post( $image['id'] );
+ if (
+ ! $att || 'attachment' !== $att->post_type
+ || ( (int) $att->post_author !== get_current_user_id() && ! current_user_can( 'edit_others_posts' ) )
+ ) {
+ continue;
+ }
+
// Update the attachment image post meta first.
$has_gallery = get_post_meta( $image['id'], '_eg_has_gallery', true );
if ( empty( $has_gallery ) ) {
@@ -241,7 +339,7 @@
function envira_gallery_ajax_sort_images() {
// Run a security check first.
- check_admin_referer( 'envira-gallery-sort', 'nonce' );
+ check_ajax_referer( 'envira-gallery-sort', 'nonce' );
// Get the Envira Gallery ID.
$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : null;
@@ -255,9 +353,24 @@
}
// Prepare variables.
- $order = isset( $_POST['order'] ) ? explode( ',', wp_unslash( $_POST['order'] ) ) : ''; // @codingStandardsIgnoreLine
+ $order = isset( $_POST['order'] ) ? array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $_POST['order'] ) ) ) ) ) : [];
$gallery_data = get_post_meta( $post_id, '_eg_gallery_data', true );
+ $existing_ids = isset( $gallery_data['gallery'] ) ? array_keys( $gallery_data['gallery'] ) : [];
+ $order = array_filter(
+ $order,
+ function ( $id ) use ( $existing_ids ) {
+ return in_array( $id, $existing_ids, true );
+ }
+ );
+
+ // Guard: if every submitted ID was filtered out but the gallery has images,
+ // something went wrong (race condition, stale tab, bad request) — abort rather
+ // than silently save an empty gallery and delete all images.
+ if ( empty( $order ) && ! empty( $existing_ids ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'No valid image IDs submitted for sorting.', 'envira-gallery-lite' ) ] );
+ }
+
// Copy the gallery config, removing the images
// Stops config from getting lost when sorting + not clicking Publish/Update.
$new_order = $gallery_data;
@@ -396,7 +509,6 @@
* @since 1.0.0
*/
function envira_gallery_ajax_save_meta() {
-
// Run a security check first.
check_ajax_referer( 'envira-gallery-save-meta', 'nonce' );
@@ -413,7 +525,8 @@
// Prepare variables.
$attach_id = isset( $_POST['attach_id'] ) ? absint( wp_unslash( $_POST['attach_id'] ) ) : null;
- $meta = isset( $_POST['meta'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['meta'] ) ) : [];
+ $raw_meta = isset( $_POST['meta'] ) ? wp_unslash( (array) $_POST['meta'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array is allowlisted to known keys below, then each value is individually sanitized (wp_kses/sanitize_text_field/esc_url_raw/wp_kses_post) before save.
+ $meta = array_intersect_key( $raw_meta, array_flip( [ 'src', 'title', 'alt', 'caption', 'link', 'link_new_window' ] ) );
$gallery_data = get_post_meta( $post_id, '_eg_gallery_data', true );
// Prevent invalid data from being saved.
@@ -458,15 +571,21 @@
}
if ( isset( $meta['alt'] ) ) {
- $gallery_data['gallery'][ $attach_id ]['alt'] = trim( esc_html( $meta['alt'] ) );
+ // sanitize_text_field() strips HTML tags without encoding; esc_attr() at render handles encoding.
+ $gallery_data['gallery'][ $attach_id ]['alt'] = sanitize_text_field( trim( $meta['alt'] ) );
}
if ( isset( $meta['link'] ) ) {
- $gallery_data['gallery'][ $attach_id ]['link'] = esc_url( $meta['link'] );
+ // Use esc_url_raw() (no HTML-encoding) so esc_url() at render doesn't double-encode &.
+ $gallery_data['gallery'][ $attach_id ]['link'] = esc_url_raw( $meta['link'] );
}
if ( isset( $meta['link_new_window'] ) ) {
- $gallery_data['gallery'][ $attach_id ]['link_new_window'] = trim( $meta['link_new_window'] );
+ $gallery_data['gallery'][ $attach_id ]['link_new_window'] = in_array( (string) $meta['link_new_window'], [ '0', '1' ], true ) ? $meta['link_new_window'] : '0';
+ }
+
+ if ( isset( $meta['caption'] ) ) {
+ $gallery_data['gallery'][ $attach_id ]['caption'] = wp_kses_post( $meta['caption'] );
}
// Allow filtering of meta before saving.
@@ -506,8 +625,38 @@
}
// Prepare variables.
- $image_ids = isset( $_POST['image_ids'] ) ? wp_unslash( $_POST['image_ids'] ) : array(); // @codingStandardsIgnoreLine - Array
- $meta = isset( $_POST['meta'] ) ? wp_unslash( $_POST['meta'] ) : array(); // @codingStandardsIgnoreLine - Array
+ $image_ids = isset( $_POST['image_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['image_ids'] ) ) : array();
+ $image_ids = array_filter( $image_ids );
+
+ // Allowlist: only accept known meta keys to prevent arbitrary data storage (blocklist approach misses unknown keys).
+ $raw_meta = isset($_POST['meta']) ? (array) wp_unslash($_POST['meta']) : array(); // @codingStandardsIgnoreLine - Array
+ $meta = array_intersect_key(
+ $raw_meta,
+ array(
+ 'title' => true,
+ 'alt' => true,
+ 'link' => true,
+ 'link_new_window' => true,
+ 'caption' => true,
+ )
+ ); // Allowlist: discard any keys not in this set.
+
+ // Sanitize each allowed field at point of ingestion to prevent stored XSS.
+ if ( isset( $meta['title'] ) ) {
+ $meta['title'] = wp_kses_post( $meta['title'] ); // Allow safe HTML (bold, italic, etc.) but strip script/event tags.
+ }
+ if ( isset( $meta['alt'] ) ) {
+ $meta['alt'] = sanitize_text_field( $meta['alt'] ); // Plain text only — alt attributes never need HTML.
+ }
+ if ( isset( $meta['link'] ) ) {
+ $meta['link'] = esc_url_raw( $meta['link'] ); // Sanitize URL for storage; esc_url() is for output only.
+ }
+ if ( isset( $meta['link_new_window'] ) ) {
+ $meta['link_new_window'] = (bool) $meta['link_new_window'] ? '1' : '0'; // Cast to boolean string to prevent arbitrary value storage.
+ }
+ if ( isset( $meta['caption'] ) ) {
+ $meta['caption'] = wp_kses_post( $meta['caption'] ); // Allow safe HTML but strip script/event tags.
+ }
// Check the required variables exist.
if ( empty( $post_id ) ) {
@@ -533,25 +682,29 @@
continue;
}
- // Update image metadata.
+ // Update image metadata. Values are already sanitized at ingestion above; assign directly.
if ( isset( $meta['title'] ) && ! empty( $meta['title'] ) ) {
- $gallery_data['gallery'][ $image_id ]['title'] = trim( $meta['title'] );
+ $gallery_data['gallery'][ $image_id ]['title'] = $meta['title']; // Already sanitized via wp_kses_post() at ingestion.
}
if ( isset( $meta['alt'] ) && ! empty( $meta['alt'] ) ) {
- $gallery_data['gallery'][ $image_id ]['alt'] = trim( esc_html( $meta['alt'] ) );
+ // $meta['alt'] already sanitize_text_field()'d above; esc_attr() at render handles encoding.
+ $gallery_data['gallery'][ $image_id ]['alt'] = trim( $meta['alt'] );
}
if ( isset( $meta['link'] ) && ! empty( $meta['link'] ) ) {
- $gallery_data['gallery'][ $image_id ]['link'] = esc_url( $meta['link'] );
+ // $meta['link'] already esc_url_raw()'d above; esc_url() at render handles encoding.
+ $gallery_data['gallery'][ $image_id ]['link'] = $meta['link'];
}
- if ( isset( $meta['link_new_window'] ) && ! empty( $meta['link_new_window'] ) ) {
- $gallery_data['gallery'][ $image_id ]['link_new_window'] = trim( $meta['link_new_window'] );
+ if ( isset( $meta['link_new_window'] ) ) {
+ // Use isset() not empty(): empty('0') is true, which would prevent saving '0' (disabled).
+ // Value already cast to '0'/'1' at ingestion above.
+ $gallery_data['gallery'][ $image_id ]['link_new_window'] = $meta['link_new_window'];
}
if ( isset( $meta['caption'] ) && ! empty( $meta['caption'] ) ) {
- $gallery_data['gallery'][ $image_id ]['caption'] = trim( $meta['caption'] );
+ $gallery_data['gallery'][ $image_id ]['caption'] = $meta['caption']; // Already sanitized via wp_kses_post() at ingestion.
}
// Allow filtering of meta before saving.
@@ -618,7 +771,7 @@
$gallery_id = isset( $_POST['post_id'] ) ? absint( sanitize_key( $_POST['post_id'] ) ) : null;
if ( ! current_user_can( 'edit_post', $gallery_id ) ) {
- wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
+ wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
}
$gallery_data = get_post_meta( $gallery_id, '_eg_gallery_data', true );
@@ -628,6 +781,27 @@
die;
}
+/**
+ * Returns the basenames of all installed plugins whose folder starts with 'envira-'.
+ * Used to restrict activate/deactivate actions to known Envira addons, preventing
+ * privilege-escalation via arbitrary plugin-path injection (High08).
+ *
+ * @return string[] Indexed array of allowed plugin basenames, e.g. ['envira-albums/envira-albums.php'].
+ */
+function envira_gallery_get_allowed_addon_plugins() {
+ // get_plugins() returns only plugins present in the WP plugins directory — no filesystem traversal possible.
+ return array_keys(
+ array_filter(
+ get_plugins(),
+ static function ( $plugin_data, $plugin_file ) {
+ // Only allow plugins whose top-level folder (or single file) starts with 'envira-'.
+ return strpos( $plugin_file, 'envira-' ) === 0;
+ },
+ ARRAY_FILTER_USE_BOTH
+ )
+ );
+}
+
add_action( 'wp_ajax_envira_gallery_install_addon', 'envira_gallery_ajax_install_addon' );
/**
* Installs an Envira addon.
@@ -645,12 +819,23 @@
// Install the addon.
if ( isset( $_POST['plugin'] ) ) {
- $download_url = esc_url_raw( wp_unslash( $_POST['plugin'] ) );
+ $download_url = esc_url_raw( wp_unslash( $_POST['plugin'] ) ); // Sanitize URL at ingestion.
+
+ // Validate URL structure (HTTPS + .zip). No domain allowlist: the caller
+ // already holds install_plugins (admin-level) and can install from any host
+ // via native WP UI. Internal-IP SSRF is blocked by WP_HTTP::block_request().
+ if ( ! envira_gallery_is_valid_plugin_download_url( $download_url ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin URL. Must be an HTTPS .zip address.', 'envira-gallery-lite' ) ] );
+ }
+
global $hook_suffix;
// Set the current screen to avoid undefined notices.
set_current_screen();
+ $method = '';
+ $url = esc_url( admin_url( 'edit.php?post_type=envira&page=envira-gallery-lite-addons' ) );
+
// Start output bufferring to catch the filesystem form if credentials are needed.
ob_start();
$creds = request_filesystem_credentials( $url, $method, false, false, null );
@@ -699,7 +884,6 @@
* @since 1.0.0
*/
function envira_gallery_ajax_activate_addon() {
-
// Run a security check first.
check_admin_referer( 'envira-gallery-activate', 'nonce' );
@@ -709,7 +893,31 @@
// Activate the addon.
if ( isset( $_POST['plugin'] ) ) {
- $activate = activate_plugin( wp_unslash( $_POST['plugin'] ) ); // @codingStandardsIgnoreLine
+ // Sanitize at ingestion; strips tags and control characters.
+ $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
+
+ // Reject path traversal sequences (e.g. "../") to prevent escaping the plugins directory.
+ if ( 0 !== validate_file( $plugin ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Restrict to the Envira addon namespace; prevents activating arbitrary installed plugins.
+ if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Confirm the plugin is actually installed; prevents targeting guessed-but-absent basenames.
+ if ( ! array_key_exists( $plugin, get_plugins() ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Plugin not found.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Reject any basename not present in the allowed addon list BEFORE activating to prevent
+ // a non-permitted plugin from being silently activated before the check fires.
+ if ( ! in_array( $plugin, envira_gallery_get_allowed_addon_plugins(), true ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Plugin not permitted.', 'envira-gallery-lite' ) ] );
+ }
+
+ $activate = activate_plugin( $plugin );
if ( is_wp_error( $activate ) ) {
echo wp_json_encode( [ 'error' => $activate->get_error_message() ] );
@@ -738,7 +946,25 @@
// Deactivate the addon.
if ( isset( $_POST['plugin'] ) ) {
- $deactivate = deactivate_plugins( wp_unslash( $_POST['plugin'] ) ); // @codingStandardsIgnoreLine
+ // Sanitize at ingestion; strips tags and control characters.
+ $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
+
+ // Reject path traversal sequences (e.g. "../") to prevent escaping the plugins directory.
+ if ( 0 !== validate_file( $plugin ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Restrict to the Envira addon namespace; prevents deactivating arbitrary installed plugins.
+ if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Confirm the plugin is currently active; prevents operating on guessed-but-absent basenames.
+ if ( ! is_plugin_active( $plugin ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Plugin not active.', 'envira-gallery-lite' ) ] );
+ }
+
+ deactivate_plugins( $plugin );
}
echo wp_json_encode( true );
@@ -773,7 +999,7 @@
'thumb' => '',
];
} else {
- $src = isset( $image['src'] ) ? $image['src'] : ( ! empty( $image['url'] ) ? $image['url'] : $url_from_src );
+ $src = ! empty( $image['src'] ) ? $image['src'] : ( ! empty( $image['url'] ) ? $image['url'] : $url_from_src );
$link = isset( $image['link'] ) && wp_http_validate_url( $image['link'] ) ? $image['link'] : $src;
$new_image = [
'status' => 'active',
@@ -797,7 +1023,6 @@
// Add image, this will default to the end of the array.
$gallery_data['gallery'][ $id ] = $image;
-
}
// Filter and return.
@@ -919,9 +1144,9 @@
}
// Get POSTed fields.
- $search = isset( $_POST['search'] ) ? (bool) wp_unslash( $_POST['search'] ) : false; // @codingStandardsIgnoreLine
- $search_terms = isset( $_POST['search_terms'] ) ? sanitize_text_field( wp_unslash( $_POST['search_terms'] ) ) : ''; // @codingStandardsIgnoreLine
- $prepend_ids = isset( $_POST['prepend_ids'] ) ? stripslashes_deep( wp_unslash( $_POST['prepend_ids'] ) ) : array(); // @codingStandardsIgnoreLine
+ $search = isset($_POST['search']) ? (bool) wp_unslash($_POST['search']) : false; // @codingStandardsIgnoreLine
+ $search_terms = isset($_POST['search_terms']) ? sanitize_text_field(wp_unslash($_POST['search_terms'])) : ''; // @codingStandardsIgnoreLine
+ $prepend_ids = isset($_POST['prepend_ids']) ? stripslashes_deep(wp_unslash($_POST['prepend_ids'])) : array(); // @codingStandardsIgnoreLine
$results = [];
// Get galleries.
@@ -1016,10 +1241,11 @@
check_admin_referer( 'envira-gallery-move-media', 'nonce' );
// Get the Envira Gallery ID.
- $from_gallery_id = isset( $_POST['from_gallery_id'] ) ? absint( wp_unslash( $_POST['from_gallery_id'] ) ) : null;
+ // absint() always returns an integer; default to 0 and test for falsiness (null check was never true).
+ $from_gallery_id = isset( $_POST['from_gallery_id'] ) ? absint( wp_unslash( $_POST['from_gallery_id'] ) ) : 0;
- if ( null === $from_gallery_id ) {
- wp_send_json_error( [ 'message' => esc_html__( 'Invalid Post ID.', 'envira-gallery-lite' ) ] );
+ if ( ! $from_gallery_id ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid From Gallery ID.', 'envira-gallery-lite' ) ] );
}
if ( ! current_user_can( 'edit_post', $from_gallery_id ) ) {
@@ -1028,7 +1254,7 @@
// Get POSTed fields.
$to_gallery_id = isset( $_POST['to_gallery_id'] ) ? absint( $_POST['to_gallery_id'] ) : null;
- $image_ids = isset( $_POST['image_ids'] ) ? wp_unslash( $_POST['image_ids'] ) : array(); // @codingStandardsIgnoreLine
+ $image_ids = isset($_POST['image_ids']) ? wp_unslash($_POST['image_ids']) : array(); // @codingStandardsIgnoreLine
if ( ! $from_gallery_id ) {
wp_send_json_error( __( 'The From Gallery ID has not been specified.', 'envira-gallery-lite' ) );
@@ -1040,19 +1266,35 @@
if ( ! current_user_can( 'edit_post', $to_gallery_id ) ) {
wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
}
- if ( count( $image_ids ) === 0 ) {
- wp_send_json_error( __( 'No images were selected to be moved between Galleries.', 'envira-gallery-lite' ) );
+
+ // Guard against a non-array POST value; would cause TypeError in PHP 8.
+ $raw_ids = isset( $_POST['image_ids'] ) ? wp_unslash( $_POST['image_ids'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below via absint().
+ if ( ! is_array( $raw_ids ) || empty( $raw_ids ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'No images were selected to be moved between Galleries.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Cast each ID to a non-negative integer and discard zeros; array_values re-indexes after filter.
+ $image_ids = array_values( array_filter( array_map( 'absint', $raw_ids ) ) );
+ if ( empty( $image_ids ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'No valid image IDs provided.', 'envira-gallery-lite' ) ] );
}
// Get from and to Galleries.
$from_gallery = Envira_Gallery::get_instance()->get_gallery( $from_gallery_id );
$to_gallery = Envira_Gallery::get_instance()->get_gallery( $to_gallery_id );
- // Iterate through each image ID, adding the image to $to_gallery, then removing from $from_gallery.
+ // get_gallery() returns false when a post has no _eg_gallery_data meta; guard before use.
+ if ( ! is_array( $from_gallery ) || ! isset( $from_gallery['gallery'] ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Source gallery data not found.', 'envira-gallery-lite' ) ] );
+ }
+ if ( ! is_array( $to_gallery ) || ! isset( $to_gallery['gallery'] ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Destination gallery data not found.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Move each image from the source gallery to the destination gallery.
foreach ( $image_ids as $image_id ) {
- // Check the image exists in $from_gallery.
- // If not, skip this image.
- if ( ! isset( $from_gallery['gallery'][ $image_id ] ) ) {
+ // Use array_key_exists rather than isset so null-valued entries still count as present.
+ if ( ! array_key_exists( $image_id, $from_gallery['gallery'] ) ) {
continue;
}
@@ -1089,7 +1331,25 @@
// Activate the addon.
if ( isset( $_POST['basename'] ) ) {
- $activate = activate_plugin( sanitize_text_field( wp_unslash( $_POST['basename'] ) ) );
+ // Sanitize; strips tags and control characters.
+ $plugin = sanitize_text_field( wp_unslash( $_POST['basename'] ) );
+
+ // Reject path traversal (e.g. "../") and absolute paths.
+ if ( 0 !== validate_file( $plugin ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Restrict to the Envira namespace; prevents targeting arbitrary plugins.
+ if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Confirm the plugin is installed before attempting activation.
+ if ( ! array_key_exists( $plugin, get_plugins() ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Plugin not found.', 'envira-gallery-lite' ) ] );
+ }
+
+ $activate = activate_plugin( $plugin );
if ( is_wp_error( $activate ) ) {
echo wp_json_encode( [ 'error' => $activate->get_error_message() ] );
@@ -1120,7 +1380,25 @@
// Deactivate the addon.
if ( isset( $_POST['basename'] ) ) {
- $deactivate = deactivate_plugins( wp_unslash( $_POST['basename'] ) ); // @codingStandardsIgnoreLine
+ // Sanitize; was wp_unslash() only before — no sanitization at all.
+ $plugin = sanitize_text_field( wp_unslash( $_POST['basename'] ) );
+
+ // Reject path traversal and absolute paths.
+ if ( 0 !== validate_file( $plugin ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Restrict to the Envira namespace; prevents deactivating arbitrary plugins.
+ if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+ }
+
+ // Confirm the plugin is currently active before deactivating.
+ if ( ! is_plugin_active( $plugin ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Plugin not active.', 'envira-gallery-lite' ) ] );
+ }
+
+ deactivate_plugins( $plugin );
}
echo wp_json_encode( true );
@@ -1146,7 +1424,15 @@
// Install the addon.
if ( isset( $_POST['download_url'] ) ) {
- $download_url = esc_url_raw( wp_unslash( $_POST['download_url'] ) );
+ $download_url = esc_url_raw( wp_unslash( $_POST['download_url'] ) ); // Sanitize URL at ingestion.
+
+ // Validate URL structure (HTTPS + .zip). No domain allowlist: the caller
+ // already holds install_plugins (admin-level) and can install from any host
+ // via native WP UI. Internal-IP SSRF is blocked by WP_HTTP::block_request().
+ if ( ! envira_gallery_is_valid_plugin_download_url( $download_url ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin URL. Must be an HTTPS .zip address.', 'envira-gallery-lite' ) ] );
+ }
+
global $hook_suffix;
// Set the current screen to avoid undefined notices.
@@ -1285,6 +1571,10 @@
$download_url = esc_url_raw( $valid_key->download_url );
+ if ( ! envira_gallery_is_valid_plugin_download_url( $download_url ) ) {
+ wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin URL. Must be an HTTPS .zip address.', 'envira-gallery-lite' ) ] );
+ }
+
// Start output bufferring to catch the filesystem form if credentials are needed.
ob_start();
--- a/envira-gallery-lite/includes/admin/common.php
+++ b/envira-gallery-lite/includes/admin/common.php
@@ -457,7 +457,9 @@
wp_register_style( $this->base->plugin_slug . '-admin-style', plugins_url( 'assets/css/admin.css', $this->base->file ), [], $this->base->version );
wp_enqueue_style( $this->base->plugin_slug . '-admin-style' );
- if ( 'envira_page_envira-gallery-settings' === $hook && isset( $_GET['post_type'] ) && 'envira' === $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ // Sanitize $_GET['post_type'] at the point of ingestion using sanitize_key() — fixes unvalidated input read (nonce verification N/A for enqueue hooks; sanitization is the correct mitigation here).
+ $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query param used only for conditional style loading, not for data mutation; sanitized above.
+ if ( 'envira_page_envira-gallery-settings' === $hook && 'envira' === $post_type ) { // Uses sanitized $post_type instead of raw $_GET value.
wp_enqueue_style( 'envira-gallery-settings-style-css', plugins_url( 'assets/css/settings.css', $this->base->file ), false, $this->base->version );
wp_enqueue_style( 'envira-choice-css', plugins_url( 'assets/css/choices.css', $this->base->file ), false, $this->base->version );
@@ -632,7 +634,12 @@
// Delete the resized image.
if ( file_exists( $file ) ) {
- @unlink( $file ); // @codingStandardsIgnoreLine
+ $real_file = realpath( $file ); // resolve canonical path — $metadata['file'] and $dims come from the DB and can be tampered with to contain ../ sequences
+ $real_basedir = realpath( $wp_upload_dir['basedir'] ); // resolve basedir with symlinks expanded for reliable prefix comparison
+ if ( $real_file && $real_basedir && 0 === strpos( $real_file, trailingslashit( $real_basedir ) ) ) {
+ // only unlink when the resolved path is confirmed inside the uploads directory
+ @unlink( $real_file ); // @codingStandardsIgnoreLine
+ }
}
}
}
--- a/envira-gallery-lite/includes/admin/menu-nudge.php
+++ b/envira-gallery-lite/includes/admin/menu-nudge.php
@@ -66,6 +66,15 @@
public function enqueue_admin_styles() {
wp_register_style( '-menu-nudge', ENVIRA_LITE_URL . 'assets/css/menu-nudge.css', [], ENVIRA_LITE_VERSION );
wp_register_script( '-menu-nudge-script', ENVIRA_LITE_URL . 'assets/js/min/menu-nudge-min.js', [ 'jquery' ], ENVIRA_LITE_VERSION, true );
+ // Localize a nonce so the JS can send it with the hide-tooltip AJAX request (Medium09 CSRF fix).
+ wp_localize_script(
+ '-menu-nudge-script',
+ 'enviraMenuNudge',
+ [
+ 'nonce' => wp_create_nonce( 'envira-hide-admin-menu-tooltip' ), // Nonce scoped to this action only.
+ 'redirectNonce' => wp_create_nonce( 'envira-redirect-to-add-new-gallery' ),
+ ]
+ );
}
/**
@@ -120,6 +129,9 @@
* Hide the admin menu tooltip.
*/
public function envira_hide_admin_menu_tooltip_callback() {
+ // Verify nonce before acting; current_user_can() alone does not prevent CSRF (Medium09 fix).
+ check_ajax_referer( 'envira-hide-admin-menu-tooltip', 'nonce' );
+
if ( current_user_can( 'manage_options' ) ) {
update_option( 'envira_admin_menu_tooltip', time() );
}
@@ -131,6 +143,7 @@
* Reload to add new page.
*/
public function envira_redirect_to_add_new_gallery_callback() {
+ check_ajax_referer( 'envira-redirect-to-add-new-gallery', 'nonce' );
if ( current_user_can( 'manage_options' ) ) {
$url = admin_url( 'post-new.php?post_type=envira' );
wp_send_json_success( [ 'redirect_url' => $url ] );
--- a/envira-gallery-lite/includes/admin/metaboxes.php
+++ b/envira-gallery-lite/includes/admin/metaboxes.php
@@ -1,5 +1,4 @@
<?php
-
/**
* Metabox class.
*
@@ -23,6 +22,7 @@
+
/**
* Holds the class object.
*
@@ -1420,17 +1420,18 @@
<p>Video platform integrations allow you to add more video sources for your galleries. We’ve added integrations with all the most popular video sharing and video hosting providers.</p>
<div class="two-column-list">
<ul>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Self-hosted Videos</a> (MP4)</li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">YouTube</a> (with playlist and custom start time support)</li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Vimeo</a></li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Instagram</a> Feed Videos</li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagesocial', 'videoplatformlinks' ) ); ?>">Instagram</a> IGTV</li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Self-hosted Videos</a> (MP4)</li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">YouTube</a> (with playlist and custom start time support)</li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Vimeo</a></li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Instagram</a> Feed Videos</li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagesocial', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Instagram</a> IGTV</li>
</ul>
<ul>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Twitch</a></li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">VideoPress</a></li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">DailyMotion</a></li>
- <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">TikTok</a></li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Twitch</a></li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">VideoPress</a></li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">DailyMotion</a></li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">TikTok</a></li>
+ <li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Google Photos</a></li>
<li><strong>...and more!</strong></li>
</ul>
</div>
@@ -1725,6 +1726,24 @@
$settings['config']['crop_width'] = isset( $_POST['_envira_gallery']['crop_width'] ) ? absint( $_POST['_envira_gallery']['crop_width'] ) > 0 ? absint( $_POST['_envira_gallery']['crop_width'] ) : $this->get_config_default( 'crop_width' ) : false;
$settings['config']['crop_height'] = isset( $_POST['_envira_gallery']['crop_height'] ) ? absint( $_POST['_envira_gallery']['crop_height'] ) > 0 ? absint( $_POST['_envira_gallery']['crop_height'] ) : $this->get_config_default( 'crop_height' ) : false;
+ // Validate type against registered types; prevents arbitrary hook names via do_action('envira_gallery_display_'.$type).
+ if ( isset( $_POST['_envira_gallery']['type'] ) ) {
+ $type = sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['type'] ) );
+ $valid_types = array_keys( $this->get_envira_types( $post ) );
+ if ( in_array( $type, $valid_types, true ) ) {
+ $settings['config']['type'] = $type;
+ }
+ }
+
+ // Validate image_size against registered sizes; prevents storing an arbitrary size key.
+ if ( isset( $_POST['_envira_gallery']['image_size'] ) ) {
+ $image_size = sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['image_size'] ) );
+ $valid_image_sizes = array_column( $this->get_image_sizes(), 'value' );
+ if ( in_array( $image_size, $valid_image_sizes, true ) ) {
+ $settings['config']['image_size'] = $image_size;
+ }
+ }
+
// Provide a filter to override settings.
$settings = apply_filters( 'envira_gallery_quick_edit_save_settings', $settings, $post_id, $post );
@@ -1767,7 +1786,9 @@
$settings['id'] = $post_id;
// Config.
- $settings['config']['type'] = isset( $_POST['_envira_gallery']['type'] ) ? sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['type'] ) ) : $this->get_config_default( 'type' );
+ $type = isset( $_POST['_envira_gallery']['type'] ) ? sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['type'] ) ) : '';
+ $valid_types = array_keys( $this->get_envira_types( $post ) );
+ $settings['config']['type'] = in_array( $type, $valid_types, true ) ? $type : $this->get_config_default( 'type' );
$settings['config']['columns'] = isset( $_POST['_envira_gallery']['columns'] ) ? preg_replace( '#[^a-z0-9-_]#', '', sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['columns'] ) ) ) : $this->get_config_default( 'columns' );
$settings['config']['gallery_theme'] = isset( $_POST['_envira_gallery']['gallery_theme'] ) ? preg_replace( '#[^a-z0-9-_]#', '', sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['gallery_theme'] ) ) ) : $this->get_config_default( 'gallery_theme' );
$settings['config']['crop_width'] = isset( $_POST['_envira_gallery']['crop_width'] ) ? absint( $_POST['_envira_gallery']['crop_width'] ) > 0 ? absint( $_POST['_envira_gallery']['crop_width'] ) : $this->get_config_default( 'crop_width' ) : $this->get_config_default( 'crop_width' );
@@ -1778,7 +1799,9 @@
$settings['config']['lazy_loading_delay'] = isset( $_POST['_envira_gallery']['lazy_loading_delay'] ) ? absint( $_POST['_envira_gallery']['lazy_loading_delay'] ) : $this->get_config_default( 'lazy_loading_delay' );
$settings['config']['gutter'] = isset( $_POST['_envira_gallery']['gutter'] ) ? absint( $_POST['_envira_gallery']['gutter'] ) : $this->get_config_default( 'gutter' );
$settings['config']['margin'] = isset( $_POST['_envira_gallery']['margin'] ) ? absint( $_POST['_envira_gallery']['margin'] ) : $this->get_config_default( 'margin' );
- $settings['config']['image_size'] = isset( $_POST['_envira_gallery']['image_size'] ) ? sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['image_size'] ) ) : $this->get_config_default( 'image_size' );
+ $size = isset( $_POST['_envira_gallery']['image_size'] ) ? sanitize_key( wp_unslash( $_POST['_envira_gallery']['image_size'] ) ) : '';
+ $valid_sizes = array_column( $this->get_image_sizes(), 'value' ); // Use get_image_sizes() to match Quick-Edit path — includes 'default' and filter-added custom sizes.
+ $settings['config']['image_size'] = in_array( $size, $valid_sizes, true ) ? $size : $this->get_config_default( 'image_size' );
// Automatic/Justified.
$settings['config']['justified_row_height'] = isset( $_POST['_envira_gallery']['justified_row_height'] ) ? absint( $_POST['_envira_gallery']['justified_row_height'] ) : 150;
@@ -2111,8 +2134,9 @@
global $id, $post;
// Get the current post ID. If ajax, grab it from the $_POST variable.
- if (defined('DOING_AJAX') && DOING_AJAX && array_key_exists('post_id', $_POST)) { // @codingStandardsIgnoreLine
- $post_id = absint(wp_unslash($_POST['post_id'])); // @codingStandardsIgnoreLine
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX && array_key_exists( 'post_id', $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- get_config() is a read-only meta lookup helper; nonce verification is the responsibility of the AJAX action handler that calls this method.
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- See note above; read-only helper, nonce belongs to calling AJAX handler. Value is sanitized with absint().
+ $post_id = absint( wp_unslash( $_POST['post_id'] ) );
} else {
$post_id = isset( $post->ID ) ? $post->ID : (int) $id;
}
--- a/envira-gallery-lite/includes/admin/notifications.php
+++ b/envira-gallery-lite/includes/admin/notifications.php
@@ -463,8 +463,8 @@
// Run a security check.
check_ajax_referer( 'envira_dismiss_notification', 'nonce' );
- // Check for access and required param.
- if ( ! $this->has_access() || empty( $_POST['id'] ) ) {
+ // Check for access and required param. Nonce already verified by check_ajax_referer() above; this line is unreachable without a valid nonce.
+ if ( ! $this->has_access() || empty( $_POST['id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via check_ajax_referer() on line 464; wp_die() is called on failure so execution never reaches here without a valid nonce.
wp_send_json_error();
}
--- a/envira-gallery-lite/includes/admin/onboarding-wizard.php
+++ b/envira-gallery-lite/includes/admin/onboarding-wizard.php
@@ -344,8 +344,8 @@
*/
public function save_onboarding_data() {
- // check for nonce enviraOnboardingCheck.
- if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'enviraOnboardingCheck' ) ) {
+ // Verify nonce — do NOT sanitize before wp_verify_nonce; sanitize_text_field() can corrupt the hash and cause false failures.
+ if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'enviraOnboardingCheck' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonces must not be sanitized before wp_verify_nonce.
wp_send_json_error( 'Invalid nonce' );
wp_die();
}
@@ -356,10 +356,13 @@
wp_die();
}
- if ( ! empty( $_POST['eow'] ) ) {
+ if ( ! empty( $_POST['eow'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via wp_verify_nonce() above; wp_die() is called on failure so execution never reaches here without a valid nonce.
// Sanitize data and merge to existing data.
$onboarding_data = get_option( 'envira_onboarding_data', [] );
+ // Capture the previous user type before it is overwritten.
+ $previous_user_type = isset( $onboarding_data['_user_type'] ) ? $onboarding_data['_user_type'] : '';
+
$onboarding_data = $this->sanitize_and_assign( '_usage_tracking', 'sanitize_text_field', $onboarding_data );
$onboarding_data = $this->sanitize_and_assign( '_email_address', 'sanitize_email', $onboarding_data );
$onboarding_data = $this->sanitize_and_assign( '_email_opt_in', 'sanitize_text_field', $onboarding_data );
@@ -369,7 +372,7 @@
if ( $updated ) {
// Send data to Drip.
- $this->save_to_drip( $onboarding_data );
+ $this->save_to_drip( $onboarding_data, $previous_user_type );
}
wp_send_json_success( 'Data saved successfully' );
@@ -409,7 +412,7 @@
*
* @return void
*/
- public function save_to_drip( array $onboarding_data ) {
+ public function save_to_drip( array $onboarding_data, string $previous_user_type = '' ) {
$url = 'https://enviragallery.com/wp-json/envira/v1/get_opt_in_data';
@@ -419,10 +422,11 @@
return;
}
- $tags = [ 'envira-lite' ];
+ $tags = [ 'envira-lite' ];
+ $current_user_type = isset( $onboarding_data['_user_type'] ) ? $onboarding_data['_user_type'] : '';
- if ( isset( $onboarding_data['_user_type'] ) ) {
- $tags[] = $onboarding_data['_user_type'];
+ if ( $current_user_type ) {
+ $tags[] = $current_user_type;
}
$body_data = [
@@ -430,6 +434,11 @@
'envira-drip-tags' => $tags,
];
+ // If the user type changed, ask the endpoint to remove the old tag.
+ if ( $previous_user_type && $previous_user_type !== $current_user_type ) {
+ $body_data['envira-drip-remove-tags'] = [ $previous_user_type ];
+ }
+
$body = wp_json_encode( $body_data );
$args = [
@@ -453,8 +462,8 @@
* Save selected addons to database.
*/
public function save_selected_addons() {
- // check for nonce enviraOnboardingCheck.
- if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'enviraOnboardingCheck' ) ) {
+ // Verify nonce — do NOT sanitize before wp_verify_nonce; sanitize_text_field() can corrupt the hash and cause false failures.
+ if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'enviraOnboardingCheck' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonces must not be sanitized before wp_verify_nonce.
wp_send_json_error( 'Invalid nonce' );
wp_die();
}
@@ -465,7 +474,7 @@
wp_die();
}
- if ( ! empty( $_POST['addons'] ) ) {
+ if ( ! empty( $_POST['addons'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via wp_verify_nonce() above; wp_die() is called on failure so execution never reaches here without a valid nonce.
$addons = explode( ',', sanitize_text_field( wp_unslash( $_POST['addons'] ) ) );
@@ -548,8 +557,8 @@
* @return void
*/
public function install_recommended_plugins() {
- // check for nonce enviraOnboardingCheck.
- if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'enviraOnboardingCheck' ) ) {
+ // Verify nonce — do NOT sanitize before wp_verify_nonce; sanitize_text_field() can corrupt the hash and cause false failures.
+ if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'enviraOnboardingCheck' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonces must not be sanitized before wp_verify_nonce.
wp_send_json_error( 'Invalid nonce' );
wp_die();
}
@@ -560,7 +569,7 @@
wp_die();
}
- if ( ! empty( $_POST['plugins'] ) ) {
+ if ( ! empty( $_POST['plugins'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via wp_verify_nonce() above; wp_die() is called on failure so execution never reaches here without a valid nonce.
// Sanitize data, plugins is a string delimited by comma.
$plugins = explode( ',', sanitize_text_field( wp_unslash( $_POST['plugins'] ) ) );
--- a/envira-gallery-lite/includes/admin/partials/metabox-gallery-type.php
+++ b/envira-gallery-lite/includes/admin/partials/metabox-gallery-type.php
@@ -72,6 +72,12 @@
<div class="title"><?php esc_html_e( 'TikTok', 'envira-gallery-lite' ); ?></div>
</a>
</li>
+ <li id="envira-gallery-type-google-photos">
+ <a href="javascript:void(0);" title="<?php esc_attr_e( 'Build Galleries from Google Photos.', 'envira-gallery-lite' ); ?>" class="link-envira-google-photos-tab upsell">
+ <div class="icon"></div>
+ <div class="title"><?php esc_html_e( 'Google Photos', 'envira-gallery-lite' ); ?></div>
+ </a>
+ </li>
</ul>
</div>
@@ -110,7 +116,7 @@
</ul>
</div>
<div class="bottom-content">
- <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlockai&utm_campaign=upgradetopro" class="button button-primary" target="_blank"><?php esc_html_e( 'Unlock AI Features', 'envira-gallery-lite' ); ?></a>
+ <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlockai&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-primary" target="_blank"><?php esc_html_e( 'Unlock AI Features', 'envira-gallery-lite' ); ?></a>
<p class="gift-text">
<?php
$image_url = esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/css/images/icons/wrapped-gift.svg' );
@@ -153,7 +159,7 @@
<div class="connector-icon">
<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
</div>
- <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_instagram&utm_campaign=upgradetopro" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+ <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_instagram&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
</a>
<div class="discount-section">
@@ -185,7 +191,7 @@
<div class="connector-icon">
<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
</div>
- <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_dribbble&utm_campaign=upgradetopro" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+ <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_dribbble&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
</a>
<div class="discount-section">
@@ -217,7 +223,39 @@
<div class="connector-icon">
<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
</div>
- <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_tiktok&utm_campaign=upgradetopro" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+ <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_tiktok&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+ <img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
+ </a>
+ <div class="discount-section">
+ <div class="discount-percentage">
+ <img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/discount-icon.png' ); ?>" alt="Discount percentage">
+ </img>
+ <span>%</span>
+ </div>
+ <p>Envira Gallery lite users <span class="offer-text">get 50% off</span> the regular price, automatically applied at checkout.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<!-- Envira Google Photos Upsell Modal -->
+<div id="envira-google-photos-upsell-modal" class="envira-addon-upsell-modal">
+ <div class="envira-addon-upsell-modal-overlay"></div>
+ <div class="envira-addon-upsell-modal-content">
+ <img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/close.png' ); ?>" alt="Close upsell modal" id="close-envira-google-photos-upsell-modal" class="close-envira-addon-upsell-modal"></img>
+
+ <div class="upsell-content-container">
+ <div class="top-content">
+ <img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/lock.png' ); ?>" alt="Lock icon" class="lock-icon"></img>
+ <h3>Google Photos is a Pro Feature</h3>
+ <p>We’re sorry, using Google Photos is not available on your plan. Please upgrade to the Pro plan to unlock all these awesome features.</p>
+ </div>
+ <div class="bottom-content">
+ <div class="connector-icon">
+ <img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
+ </div>
+ <a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_google-photos&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
</a>
<div class="discount-section">
--- a/envira-gallery-lite/includes/admin/partials/onboarding-wizard/step-1.php
+++ b/envira-gallery-lite/includes/admin/partials/onboarding-wizard/step-1.php
@@ -53,7 +53,7 @@
</div>
</div>
<div class="envira-options" id="others_div" style="display: none;">
- <input type="text" id="others" name="eow[_others]" value="<?php echo esc_attr( $onboarding->get_onboarding_data( '_others' ) ); ?>" placeholder="<?php esc_attr_e( 'What best describes you?', 'envira-gallery-lite' ); ?>">
+ <input type="text" id="others" name="eow[_others]" required value="<?php echo esc_attr( $onboarding->get_onboarding_data( '_others' ) ); ?>" placeholder="<?php esc_attr_e( 'What best describes you?', 'envira-gallery-lite' ); ?>">
</div>
</div>
</div>
--- a/envira-