Changeset 3737477
- Timestamp:
- 10/10/2026 01:03:33 AM (7 days ago)
- Location:
- wplr-sync/trunk
- Files:
-
- 4 edited
-
classes/api.php (modified) (11 diffs)
-
classes/core.php (modified) (2 diffs)
-
readme.txt (modified) (2 diffs)
-
wplr-sync.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
wplr-sync/trunk/classes/api.php
r3714439 r3737477 40 40 // Upload 41 41 else if ( isset( $_POST['action'] ) && $_POST['action'] === 'sync' ) { 42 if ( !$user = $this->auth( $_POST['token']) ) {42 if ( !$user = $this->auth( isset( $_POST['token'] ) ? wp_unslash( $_POST['token'] ) : null ) ) { 43 43 $this->response( null, false, $this->error ); 44 44 $this->finalize(); … … 51 51 $body = file_get_contents('php://input'); 52 52 $args = json_decode( $body ); 53 if ( !$user = $this->auth( $args->token ) ) { 53 if ( !is_object( $args ) || !isset( $args->action ) ) { 54 $this->response( null, false, "Invalid request." ); 55 $this->finalize(); 56 } 57 if ( !$user = $this->auth( isset( $args->token ) ? $args->token : null ) ) { 54 58 $this->response( null, false, $this->error ); 55 59 $this->finalize(); 56 60 } 57 61 $wplr->log( "Action: " . $args->action ); 58 if ( !$user = $this->auth( $args->token ) ) 59 $this->response( null, false, $this->error ); 60 else if ( $args->action === 'presync' ) 62 if ( $args->action === 'presync' ) 61 63 $this->presync( $args ); 62 64 else if ( $args->action === 'sync_collection' ) … … 117 119 global $wplr, $wpdb; 118 120 119 120 if ( empty( $token ) ) { 121 // Failed attempts are counted per IP: a token can't be guessed by hammering the endpoint. 122 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; 123 $fail_key = 'wplr_api_fail_' . md5( $ip ); 124 $fails = (int) get_transient( $fail_key ); 125 if ( $fails >= 20 ) { 126 $this->error = "Too many failed attempts. Please try again in 15 minutes."; 127 $wplr->log( $this->error ); 128 return false; 129 } 130 131 if ( !is_string( $token ) || strlen( $token ) < 12 || strlen( $token ) > 128 ) { 121 132 $this->error = "Authentification is missing."; 122 133 $wplr->log( $this->error ); 134 set_transient( $fail_key, $fails + 1, 15 * MINUTE_IN_SECONDS ); 123 135 return false; 124 136 } 125 126 //* DEBUG: allow to authenticate as admin with a hardcoded token (for testing purposes only)127 // if ( $token == "MEOW_TOKEN" ) {128 129 // $this->user = get_userdata( 1 );130 // if ( empty( $this->user ) ) {131 // $this->error = "User admin does not exist.";132 // $wplr->log( $this->error );133 // return false;134 // }135 136 // $wplr->log( "Authenticated as admin" );137 // return $this->user;138 // }139 137 140 138 $prefix = defined( 'BLOG_ID_CURRENT_SITE' ) ? $wpdb->get_blog_prefix( BLOG_ID_CURRENT_SITE ) : $wpdb->prefix; 141 139 $tbl_meta = defined('CUSTOM_USER_META_TABLE' ) ? CUSTOM_USER_META_TABLE : $prefix . "usermeta"; 142 $ userId = $wpdb->get_var( $wpdb->prepare( "SELECT user_idFROM $tbl_meta140 $row = $wpdb->get_row( $wpdb->prepare( "SELECT user_id, meta_value FROM $tbl_meta 143 141 WHERE meta_value = %s AND meta_key = %s", $token, 'wplr_auth_token' ) ); 144 if ( empty( $userId ) ) { 145 $this->error = "Token '$token' seems to be invalid."; 142 // The SQL match can be case-insensitive (collation): the token must match exactly. 143 if ( empty( $row ) || !is_string( $row->meta_value ) || !hash_equals( $row->meta_value, $token ) ) { 144 $this->error = "This token seems to be invalid."; 146 145 $wplr->log( $this->error ); 146 set_transient( $fail_key, $fails + 1, 15 * MINUTE_IN_SECONDS ); 147 147 return false; 148 148 } 149 $this->user = get_userdata( $ userId );149 $this->user = get_userdata( $row->user_id ); 150 150 if ( empty( $this->user ) ) { 151 151 $this->error = 'User does not exist.'; … … 153 153 return false; 154 154 } 155 156 // Act as this user, so every capability check below applies to them. 157 wp_set_current_user( $this->user->ID ); 158 if ( !current_user_can( 'upload_files' ) ) { 159 $this->error = 'This user is not allowed to upload files.'; 160 $wplr->log( $this->error ); 161 $this->user = null; 162 return false; 163 } 155 164 $wplr->log( "Authenticated as " . $this->user->user_login ); 156 165 return $this->user; 166 } 167 168 // The attachment(s) behind a Lightroom ID must be editable (or deletable) by the authenticated user. 169 function can_on_lr_id( $lr_id, $cap ) { 170 global $wplr; 171 foreach ( (array) $wplr->list_wpids( (int) $lr_id ) as $row ) { 172 if ( !empty( $row->wp_id ) && !current_user_can( $cap, (int) $row->wp_id ) ) { 173 return false; 174 } 175 } 176 return true; 177 } 178 179 function can_on_wp_id( $wp_id, $cap ) { 180 $wp_id = (int) $wp_id; 181 return $wp_id > 0 && get_post_type( $wp_id ) === 'attachment' && current_user_can( $cap, $wp_id ); 157 182 } 158 183 … … 237 262 $file = $_FILES['file']['tmp_name']; 238 263 264 // Syncing over an existing photo replaces its file: the user must be allowed to edit it. 265 if ( !$this->can_on_lr_id( $args["id"], 'edit_post' ) ) { 266 return $this->response( null, false, 'You are not allowed to edit this media.' ); 267 } 268 239 269 if ( !isset( $args["wp_col_id"] ) || $args["wp_col_id"] == null ) 240 270 $args["wp_col_id"] = -1; … … 301 331 global $wplr; 302 332 $res = false; 333 // Deleting a collection also deletes its photos, which may belong to anyone. 334 if ( !current_user_can( 'delete_others_posts' ) ) { 335 return $this->response( null, false, 'You are not allowed to delete collections.' ); 336 } 303 337 if ( property_exists( $args, 'remoteId' ) ) 304 338 $res = $wplr->delete_collection( $args->remoteId ); … … 312 346 if ( property_exists( $args, 'collection_id' ) ) 313 347 $collectionId = $args->collection_id; 314 $list = $wplr->delete_media( $args->lr_id, $collectionId ); 348 if ( !property_exists( $args, 'lr_id' ) || !$this->can_on_lr_id( $args->lr_id, 'delete_post' ) ) { 349 return $this->response( null, false, 'You are not allowed to delete this media.' ); 350 } 351 $list = $wplr->delete_media( (int) $args->lr_id, $collectionId ); 315 352 return $this->response( $list ); 316 353 } … … 335 372 function unlink( $args ) { 336 373 global $wplr; 337 $res = $wplr->unlink_media( $args->lr_id, $args->wp_id ); 374 $wp_id = property_exists( $args, 'wp_id' ) ? (int) $args->wp_id : 0; 375 $lr_id = property_exists( $args, 'lr_id' ) ? (int) $args->lr_id : 0; 376 if ( $wp_id ? !$this->can_on_wp_id( $wp_id, 'edit_post' ) : !$this->can_on_lr_id( $lr_id, 'edit_post' ) ) { 377 return $this->response( null, false, 'You are not allowed to edit this media.' ); 378 } 379 $res = $wplr->unlink_media( $lr_id, $wp_id ); 338 380 return $this->response( $res ); 339 381 } … … 342 384 function link( $args ) { 343 385 global $wplr; 344 $res = $wplr->link_media( $args->lr_id, $args->wp_id ); 386 // Linking decides which attachment a later sync or delete will touch. 387 $wp_id = property_exists( $args, 'wp_id' ) ? (int) $args->wp_id : 0; 388 if ( !$this->can_on_wp_id( $wp_id, 'edit_post' ) ) { 389 return $this->response( null, false, 'You are not allowed to link this media.' ); 390 } 391 $res = $wplr->link_media( (int) $args->lr_id, $wp_id ); 345 392 return $this->response( $res ); 346 393 } … … 357 404 function linkinfo( $args ) { 358 405 global $wplr; 359 $linkinfo = $wplr->linkinfo_media( $args->wp_id ); 406 if ( !$this->can_on_wp_id( property_exists( $args, 'wp_id' ) ? $args->wp_id : 0, 'edit_post' ) ) { 407 return $this->response( null, false, 'You are not allowed to access this media.' ); 408 } 409 $linkinfo = $wplr->linkinfo_media( (int) $args->wp_id ); 360 410 return $this->response( $linkinfo ); 361 411 } … … 365 415 global $wplr; 366 416 $wp_ids = isset( $args->wp_ids ) ? (array)$args->wp_ids : array(); 417 $wp_ids = array_values( array_filter( array_map( 'intval', $wp_ids ), function ( $id ) { 418 return $this->can_on_wp_id( $id, 'edit_post' ); 419 } ) ); 367 420 $list = $wplr->linkinfo_bulk( $wp_ids ); 368 421 return $this->response( $list ); -
wplr-sync/trunk/classes/core.php
r3714439 r3737477 260 260 261 261 function profile_update( $user_id ) { 262 // Only users who can upload get a Lightroom token; the others have no use for one. 263 if ( !user_can( $user_id, 'upload_files' ) ) { 264 return; 265 } 262 266 $token = get_user_meta( $user_id, 'wplr_auth_token', true ); 263 267 if ( empty( $token ) ) { … … 272 276 */ 273 277 function generate_auth_token( $userId ) { 274 static $MIN_LENGTH = 24; 275 static $MAX_LENGTH = 32; 276 277 // Seed 278 list( $usec, $sec ) = explode( ' ', microtime() ); 279 $seed = $sec + $usec * 1000000; 280 srand( $seed ); 281 282 // Compose 283 $r = ''; 284 $chars = str_split( '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ); 285 $nChars = count( $chars ); 286 $length = rand( $MIN_LENGTH, $MAX_LENGTH ); 287 for ( $i = 0; $i < $length; $i++ ) $r .= $chars[rand( 0, $nChars - 1 )]; 278 // A cryptographically secure token. It used to come from rand() seeded with the time, 279 // which made it guessable from the moment it was created. 280 $r = wp_generate_password( 32, false, false ); 288 281 289 282 // Save -
wplr-sync/trunk/readme.txt
r3714439 r3737477 7 7 Tested up to: 7.1 8 8 Requires PHP: 8.1 9 Stable tag: 6.5. 69 Stable tag: 6.5.7 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 63 63 64 64 == Changelog == 65 66 = 6.5.7 (2026/10/10) = 67 * Fix: Security: The Lightroom sync API now requires a user who can upload files and checks their rights on every photo it edits, links or deletes. 68 * Fix: Security: Lightroom sync tokens are now unguessable and repeated failed attempts are blocked. 69 * 🎵 Discuss with others about Wplr Sync on [the Discord](https://discord.gg/bHDGh38). 70 * 🌴 Keep us motivated with [a little review here](https://wordpress.org/support/plugin/wplr-sync/reviews/). Thank you! 71 * 🥰 If you want to help us, check our [Patreon](https://www.patreon.com/meowapps). Thank you! 65 72 66 73 = 6.5.6 (2026/09/26) = -
wplr-sync/trunk/wplr-sync.php
r3714439 r3737477 4 4 Plugin URI: https://meowapps.com 5 5 Description: Synchronize and maintain your photos, collections, keywords and metadata between Lightroom and WordPress. 6 Version: 6.5. 66 Version: 6.5.7 7 7 Author: Jordy Meow 8 8 Author URI: https://meowapps.com … … 15 15 */ 16 16 17 define( 'WPLR_SYNC_VERSION', '6.5. 6' );17 define( 'WPLR_SYNC_VERSION', '6.5.7' ); 18 18 define( 'WPLR_SYNC_PREFIX', 'wplr_sync' ); 19 19 define( 'WPLR_SYNC_DOMAIN', 'wplr-sync' );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)