diff --git a/README.md b/README.md index 512d483c0..b1cd6edfd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Add albums, photo, audio/video upload, privacy, sharing, front-end uploads & mor ![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=MU1JamdmRnppK0hhQy9QMU8wdDJ2MUEyb1ZuS0ljVFQvSHZ6anFvNzUxTT0tLXhUNnliTnZGcE5CcW93N0I1eXdnM3c9PQ==--8c124e667dd0c317618efde1bed2b260000916b6) -* **Contributors:** [rtcamp](http://profiles.wordpress.org/rtcamp), [mangeshp](http://profiles.wordpress.org/mangeshp), [sanket.parmar](http://profiles.wordpress.org/sanket.parmar), [pranalipatel](http://profiles.wordpress.org/pranalipatel), [jignesh.nakrani](http://profiles.wordpress.org/jignesh.nakrani), [manishsongirkar36](http://profiles.wordpress.org/manishsongirkar36), [kiranpotphode](http://profiles.wordpress.org/kiranpotphode), [yahil](http://profiles.wordpress.org/yahil), [1naveengiri](http://profiles.wordpress.org/1naveengiri), [bhargavbhandari90](http://profiles.wordpress.org/bhargavbhandari90), [deepak1191](http://profiles.wordpress.org/deepak1191), [rittesh.patel](http://profiles.wordpress.org/rittesh.patel), [sagarjadhav](http://profiles.wordpress.org/sagarjadhav), [pushpak.pop](http://profiles.wordpress.org/pushpak.pop), [faishal](http://profiles.wordpress.org/faishal), [desaiuditd](http://profiles.wordpress.org/desaiuditd), [rahul286](http://profiles.wordpress.org/rahul286), [JoshuaAbenazer](http://profiles.wordpress.org/JoshuaAbenazer), [gagan0123](http://profiles.wordpress.org/gagan0123), [saurabhshukla](http://profiles.wordpress.org/saurabhshukla), [nitun.lanjewar](http://profiles.wordpress.org/nitun.lanjewar), [umesh.nevase](http://profiles.wordpress.org/umesh.nevase), [suhasgirgaonkar](http://profiles.wordpress.org/suhasgirgaonkar), [neerukoul](http://profiles.wordpress.org/neerukoul), [hrishiv90](http://profiles.wordpress.org/hrishiv90), [kanakiyajay](http://profiles.wordpress.org/kanakiyajay), [jarretc](http://profiles.wordpress.org/jarretc), [tobiaskluge](http://profiles.wordpress.org/tobiaskluge), [rafaelfunchal](http://profiles.wordpress.org/rafaelfunchal), [UmeshSingla](http://profiles.wordpress.org/UmeshSingla), [mehulkaklotar](http://profiles.wordpress.org/mehulkaklotar), [tannermirabel](http://profiles.wordpress.org/tannermirabel), [kishores](http://profiles.wordpress.org/kishores), [chandrapatel](http://profiles.wordpress.org/chandrapatel), [rahul3883](http://profiles.wordpress.org/rahul3883/), [nomnom99](http://profiles.wordpress.org/nomnom99), [sayanchakraborty](https://profiles.wordpress.org/sayanchakraborty), [milindmore22](https://profiles.wordpress.org/milindmore22), [thrijith](https://profiles.wordpress.org/thrijith), [abhijitrakas](https://profiles.wordpress.org/abhijitrakas), [sid177](https://profiles.wordpress.org/sid177), [montu3366](https://profiles.wordpress.org/montu3366), [jashwini](https://profiles.wordpress.org/jashwini), [juhise](https://profiles.wordpress.org/juhise), [ravatparmar](https://profiles.wordpress.org/ravatparmar), [dharmin16](https://profiles.wordpress.org/dharmin16), [malavvasita](https://profiles.wordpress.org/malavvasita), [pooja1210](https://profiles.wordpress.org/pooja1210), [krupajnanda](https://profiles.wordpress.org/krupajnanda), [kanumalivad](https://profiles.wordpress.org/kanumalivad), [surajkumarsingh](https://profiles.wordpress.org/surajkumarsingh), [dishitpala](https://profiles.wordpress.org/dishitpala), [shobhit2412](https://profiles.wordpress.org/shobhit2412/), [vkd007](https://profiles.wordpress.org/vkd007/), [vaishu.agola27](https://profiles.wordpress.org/vaishuagola27/), [kapilpaul](https://profiles.wordpress.org/kapilpaul/), [opurockey](https://profiles.wordpress.org/opurockey/), [pavanpatil1](https://profiles.wordpress.org/pavanpatil1/), [pradeep1308](https://profiles.wordpress.org/pradeep1308/), [shardul200](https://profiles.wordpress.org/shardul200/), [ibnulk](https://profiles.wordpress.org/ibnulk/), [sabbir1991](https://profiles.wordpress.org/sabbir1991/), [kamalahmed](https://profiles.wordpress.org/kamalahmed/), [harshbarach](https://profiles.wordpress.org/harshbarach/), [mukulsingh27](https://profiles.wordpress.org/mukulsingh27/), [vishalkakadiya](https://profiles.wordpress.org/vishalkakadiya/), [elifvish](https://profiles.wordpress.org/elifvish/), [krupajnanda](https://profiles.wordpress.org/krupajnanda/), [utsavladani](https://profiles.wordpress.org/utsavladani/),[krishana79](https://profiles.wordpress.org/krishana79/), [rohitmathur7](https://profiles.wordpress.org/rohitmathur7/), [kuldipchaudhary](https://profiles.wordpress.org/kuldipchaudhary/), [mchirag2002](https://profiles.wordpress.org/mchirag2002/), [vedantgandhi28](https://profiles.wordpress.org/vedantgandhi28/), [mohamedahamed](https://profiles.wordpress.org/mohamedahamed/) +* **Contributors:** [rtcamp](http://profiles.wordpress.org/rtcamp), [mangeshp](http://profiles.wordpress.org/mangeshp), [sanket.parmar](http://profiles.wordpress.org/sanket.parmar), [pranalipatel](http://profiles.wordpress.org/pranalipatel), [jignesh.nakrani](http://profiles.wordpress.org/jignesh.nakrani), [manishsongirkar36](http://profiles.wordpress.org/manishsongirkar36), [kiranpotphode](http://profiles.wordpress.org/kiranpotphode), [yahil](http://profiles.wordpress.org/yahil), [1naveengiri](http://profiles.wordpress.org/1naveengiri), [bhargavbhandari90](http://profiles.wordpress.org/bhargavbhandari90), [deepak1191](http://profiles.wordpress.org/deepak1191), [rittesh.patel](http://profiles.wordpress.org/rittesh.patel), [sagarjadhav](http://profiles.wordpress.org/sagarjadhav), [pushpak.pop](http://profiles.wordpress.org/pushpak.pop), [faishal](http://profiles.wordpress.org/faishal), [desaiuditd](http://profiles.wordpress.org/desaiuditd), [rahul286](http://profiles.wordpress.org/rahul286), [JoshuaAbenazer](http://profiles.wordpress.org/JoshuaAbenazer), [gagan0123](http://profiles.wordpress.org/gagan0123), [saurabhshukla](http://profiles.wordpress.org/saurabhshukla), [nitun.lanjewar](http://profiles.wordpress.org/nitun.lanjewar), [umesh.nevase](http://profiles.wordpress.org/umesh.nevase), [suhasgirgaonkar](http://profiles.wordpress.org/suhasgirgaonkar), [neerukoul](http://profiles.wordpress.org/neerukoul), [hrishiv90](http://profiles.wordpress.org/hrishiv90), [kanakiyajay](http://profiles.wordpress.org/kanakiyajay), [jarretc](http://profiles.wordpress.org/jarretc), [tobiaskluge](http://profiles.wordpress.org/tobiaskluge), [rafaelfunchal](http://profiles.wordpress.org/rafaelfunchal), [UmeshSingla](http://profiles.wordpress.org/UmeshSingla), [mehulkaklotar](http://profiles.wordpress.org/mehulkaklotar), [tannermirabel](http://profiles.wordpress.org/tannermirabel), [kishores](http://profiles.wordpress.org/kishores), [chandrapatel](http://profiles.wordpress.org/chandrapatel), [rahul3883](http://profiles.wordpress.org/rahul3883/), [nomnom99](http://profiles.wordpress.org/nomnom99), [sayanchakraborty](https://profiles.wordpress.org/sayanchakraborty), [milindmore22](https://profiles.wordpress.org/milindmore22), [thrijith](https://profiles.wordpress.org/thrijith), [abhijitrakas](https://profiles.wordpress.org/abhijitrakas), [sid177](https://profiles.wordpress.org/sid177), [montu3366](https://profiles.wordpress.org/montu3366), [jashwini](https://profiles.wordpress.org/jashwini), [juhise](https://profiles.wordpress.org/juhise), [ravatparmar](https://profiles.wordpress.org/ravatparmar), [dharmin16](https://profiles.wordpress.org/dharmin16), [malavvasita](https://profiles.wordpress.org/malavvasita), [pooja1210](https://profiles.wordpress.org/pooja1210), [krupajnanda](https://profiles.wordpress.org/krupajnanda), [kanumalivad](https://profiles.wordpress.org/kanumalivad), [surajkumarsingh](https://profiles.wordpress.org/surajkumarsingh), [dishitpala](https://profiles.wordpress.org/dishitpala), [shobhit2412](https://profiles.wordpress.org/shobhit2412/), [vkd007](https://profiles.wordpress.org/vkd007/), [vaishu.agola27](https://profiles.wordpress.org/vaishuagola27/), [kapilpaul](https://profiles.wordpress.org/kapilpaul/), [opurockey](https://profiles.wordpress.org/opurockey/), [pavanpatil1](https://profiles.wordpress.org/pavanpatil1/), [pradeep1308](https://profiles.wordpress.org/pradeep1308/), [shardul200](https://profiles.wordpress.org/shardul200/), [ibnulk](https://profiles.wordpress.org/ibnulk/), [sabbir1991](https://profiles.wordpress.org/sabbir1991/), [kamalahmed](https://profiles.wordpress.org/kamalahmed/), [harshbarach](https://profiles.wordpress.org/harshbarach/), [mukulsingh27](https://profiles.wordpress.org/mukulsingh27/), [vishalkakadiya](https://profiles.wordpress.org/vishalkakadiya/), [elifvish](https://profiles.wordpress.org/elifvish/), [krupajnanda](https://profiles.wordpress.org/krupajnanda/), [utsavladani](https://profiles.wordpress.org/utsavladani/),[krishana79](https://profiles.wordpress.org/krishana79/), [rohitmathur7](https://profiles.wordpress.org/rohitmathur7/), [kuldipchaudhary](https://profiles.wordpress.org/kuldipchaudhary/), [mchirag2002](https://profiles.wordpress.org/mchirag2002/), [vedantgandhi28](https://profiles.wordpress.org/vedantgandhi28/), [mohamedahamed](https://profiles.wordpress.org/mohamedahamed/),[thehercules](https://profiles.wordpress.org/thehercules/), [suryakantupadhyay](https://profiles.wordpress.org/suryakantupadhyay/) * **License:** [GPL v2 or later]( http://www.gnu.org/licenses/gpl-2.0.html) @@ -147,6 +147,13 @@ https://www.youtube.com/watch?v=dJrykKQGDcs ## Changelog ## +### 4.7.13 + +* FIXED + * Fixed insecure API token generation, validation and expiration. + * Fixed insufficient brute-force protection for API authentication and token validation. + * Fixed username enumeration through API login error responses. + ### 4.7.12 * FIXED * Fixed authorization checks for media, albums, comments and activity privacy so actions are limited to permitted users. @@ -1049,4 +1056,4 @@ rtMedia uses the following projects/sources for some functionality * [Magnific Popup](http://dimsemenov.com/plugins/magnific-popup/) for responsive lightbox * [getID3](http://getid3.sourceforge.net/) gets us some ID tags for the media * [Foundation](http://foundation.zurb.com/) for the media grid and layout -* [Backbone.js](http://backbonejs.org/) for an MVC architecture for the frontend +* [Backbone.js](http://backbonejs.org/) for an MVC architecture for the frontend \ No newline at end of file diff --git a/app/main/controllers/api/RTMediaApiRateLimiter.php b/app/main/controllers/api/RTMediaApiRateLimiter.php new file mode 100644 index 000000000..5d2aba521 --- /dev/null +++ b/app/main/controllers/api/RTMediaApiRateLimiter.php @@ -0,0 +1,402 @@ +get_login_retry_after( $identifier ); + } + + /** + * Get the remaining time for blocked login buckets. + * + * When both the client IP and identifier buckets are blocking, the client + * must wait until both have expired, so return the longer remaining TTL. + * + * @param string $identifier Username or email address supplied for login. + * + * @return int Remaining seconds, or zero when the login is not blocked. + */ + public function get_login_retry_after( $identifier ) { + $ip_key = $this->get_key( 'login_ip', $this->get_client_ip() ); + $identifier_key = $this->get_key( 'login_id', $this->normalize_identifier( $identifier ) ); + $retry_after = 0; + + if ( $this->get_attempts( $ip_key ) >= $this->get_login_ip_limit() ) { + $retry_after = max( 1, $this->get_remaining_ttl( $ip_key ) ); + } + + if ( $this->get_attempts( $identifier_key ) >= $this->get_login_identifier_limit() ) { + $retry_after = max( $retry_after, 1, $this->get_remaining_ttl( $identifier_key ) ); + } + + return $retry_after; + } + + /** + * Record a failed login attempt. + * + * @param string $identifier Username or email address supplied for login. + */ + public function record_login_failure( $identifier ) { + $this->increment( $this->get_key( 'login_ip', $this->get_client_ip() ) ); + $this->increment( $this->get_key( 'login_id', $this->normalize_identifier( $identifier ) ) ); + } + + /** + * Clear the identifier bucket after a successful login. + * + * The IP bucket is deliberately retained so an attacker cannot reset it by + * successfully authenticating an account they control. + * + * @param string $identifier Username or email address supplied for login. + */ + public function clear_login_identifier( $identifier ) { + $this->delete( $this->get_key( 'login_id', $this->normalize_identifier( $identifier ) ) ); + } + + /** + * Check whether invalid token submissions should be blocked for this client. + * + * @return bool + */ + public function is_token_validation_blocked() { + return 0 < $this->get_token_retry_after(); + } + + /** + * Get the remaining time for a blocked token-validation bucket. + * + * @return int Remaining seconds, or zero when token validation is not blocked. + */ + public function get_token_retry_after() { + $key = $this->get_key( 'token_ip', $this->get_client_ip() ); + + if ( $this->get_attempts( $key ) < $this->get_token_limit() ) { + return 0; + } + + return max( 1, $this->get_remaining_ttl( $key ) ); + } + + /** + * Record an invalid token submission. + */ + public function record_token_failure() { + $this->increment( $this->get_key( 'token_ip', $this->get_client_ip() ) ); + } + + /** + * Return the rate-limit window in seconds. + * + * @return int + */ + public function get_window() { + $window = (int) apply_filters( 'rtmedia_api_rate_limit_window', self::DEFAULT_WINDOW ); + + return max( MINUTE_IN_SECONDS, $window ); + } + + /** + * Increment a rate-limit bucket and return the new count. + * + * When a persistent object cache is active (Redis, Memcached, etc.), the + * increment is atomic: wp_cache_add() seeds the key exactly once and + * wp_cache_incr() is a single atomic operation on the cache backend, so + * concurrent requests cannot race on a read-modify-write cycle. + * + * Without a persistent object cache, this falls back to a transient + * (stored in wp_options). That fallback is NOT atomic: two concurrent + * requests can both read the same count and both write count+1, silently + * losing an increment. + * + * @param string $key Bucket key. + * + * @return int + */ + private function increment( $key ) { + $window = $this->get_window(); + + if ( wp_using_ext_object_cache() ) { + $now = time(); + $expiry_key = $this->get_expiry_key( $key ); + + // Seed the key only if absent; wp_cache_add() itself is atomic + // (no-op if the key already exists), so this never clobbers an + // in-flight counter from a concurrent request. + $counter_added = wp_cache_add( $key, 0, self::CACHE_GROUP, $window ); + + // WordPress does not expose a cached key's remaining TTL. Store + // the absolute expiry separately so Retry-After can be accurate. + if ( $counter_added ) { + wp_cache_set( $expiry_key, $now + $window, self::CACHE_GROUP, $window ); + } else { + wp_cache_add( $expiry_key, $now + $window, self::CACHE_GROUP, $window ); + } + + $count = wp_cache_incr( $key, 1, self::CACHE_GROUP ); + + // wp_cache_incr() can return false if the key expired between + // the add() above and the incr() call. Reseed at 1 in that case. + if ( false === $count ) { + wp_cache_set( $key, 1, self::CACHE_GROUP, $window ); + wp_cache_set( $expiry_key, $now + $window, self::CACHE_GROUP, $window ); + $count = 1; + } + + return (int) $count; + } + + // Transient fallback — see race-condition note in the docblock above. + $now = time(); + $bucket = get_transient( $key ); + + if ( + ! is_array( $bucket ) + || ! isset( $bucket['count'], $bucket['expires_at'] ) + || ! is_numeric( $bucket['count'] ) + || ! is_numeric( $bucket['expires_at'] ) + || (int) $bucket['expires_at'] <= $now + ) { + $bucket = array( + 'count' => 0, + 'expires_at' => $now + $window, + ); + } + + $count = (int) $bucket['count'] + 1; + $bucket['count'] = $count; + $remaining = max( 1, (int) $bucket['expires_at'] - time() ); + set_transient( $key, $bucket, $remaining ); + + return $count; + } + + /** + * Get the number of attempts in a bucket. + * + * @param string $key Bucket key. + * + * @return int + */ + private function get_attempts( $key ) { + if ( wp_using_ext_object_cache() ) { + $count = wp_cache_get( $key, self::CACHE_GROUP ); + + return false === $count ? 0 : (int) $count; + } + + $bucket = get_transient( $key ); + + if ( + ! is_array( $bucket ) + || ! isset( $bucket['count'], $bucket['expires_at'] ) + || ! is_numeric( $bucket['count'] ) + || ! is_numeric( $bucket['expires_at'] ) + || (int) $bucket['expires_at'] <= time() + ) { + return 0; + } + + return (int) $bucket['count']; + } + + /** + * Get the remaining lifetime of a bucket. + * + * @param string $key Bucket key. + * + * @return int Remaining seconds, or zero when no valid expiry is available. + */ + private function get_remaining_ttl( $key ) { + if ( wp_using_ext_object_cache() ) { + $expires_at = wp_cache_get( $this->get_expiry_key( $key ), self::CACHE_GROUP ); + + // Counters created before expiry metadata was introduced may remain + // active for one window. Use the full window as a safe fallback. + if ( false === $expires_at || ! is_numeric( $expires_at ) ) { + return $this->get_window(); + } + + return max( 0, (int) $expires_at - time() ); + } + + $bucket = get_transient( $key ); + + if ( + ! is_array( $bucket ) + || ! isset( $bucket['expires_at'] ) + || ! is_numeric( $bucket['expires_at'] ) + ) { + return 0; + } + + return max( 0, (int) $bucket['expires_at'] - time() ); + } + + /** + * Delete a bucket, regardless of backend. + * + * @param string $key Bucket key. + */ + private function delete( $key ) { + if ( wp_using_ext_object_cache() ) { + wp_cache_delete( $key, self::CACHE_GROUP ); + wp_cache_delete( $this->get_expiry_key( $key ), self::CACHE_GROUP ); + + return; + } + + delete_transient( $key ); + } + + /** + * Create a bounded, non-reversible bucket key. + * + * @param string $scope Rate-limit scope. + * @param string $identifier Client or login identifier. + * + * @return string + */ + private function get_key( $scope, $identifier ) { + return 'rtm_api_rl_' . $scope . '_' . substr( wp_hash( (string) $identifier, 'auth' ), 0, 32 ); + } + + /** + * Get the companion object-cache key for a bucket's absolute expiry. + * + * @param string $key Bucket key. + * + * @return string + */ + private function get_expiry_key( $key ) { + return $key . self::EXPIRY_KEY_SUFFIX; + } + + /** + * Get the client IP address to key rate limits on. + * + * By default only REMOTE_ADDR is trusted. Proxy headers such as + * X-Forwarded-For or CF-Connecting-IP are never read here because they + * can be forged by the client unless a specific, known reverse proxy + * strips and rewrites them first — and this plugin has no way to know + * whether that's true for any given install. + * + * @return string + */ + private function get_client_ip() { + $remote_addr = rtm_get_server_var( 'REMOTE_ADDR', 'FILTER_VALIDATE_IP' ); + $remote_addr = $remote_addr ? $remote_addr : 'unknown'; + + /** + * Filters the client IP address used for API rate limiting. + * + * @param string $remote_addr The address read from REMOTE_ADDR. + */ + $client_ip = apply_filters( 'rtmedia_api_client_ip', $remote_addr ); + + $client_ip = is_string( $client_ip ) ? trim( $client_ip ) : ''; + + // Validate whatever the filter returned; never trust it blindly, + // since a badly written filter could hand back attacker-controlled + // input straight from an unvalidated header. + return filter_var( $client_ip, FILTER_VALIDATE_IP ) ? $client_ip : 'unknown'; + } + + /** + * Normalize a login identifier before deriving its rate-limit key. + * + * @param string $identifier Username or email address. + * + * @return string + */ + private function normalize_identifier( $identifier ) { + return strtolower( trim( (string) $identifier ) ); + } + + /** + * Get the maximum login failures allowed per IP address. + * + * @return int + */ + private function get_login_ip_limit() { + return max( 1, (int) apply_filters( 'rtmedia_api_login_ip_limit', self::DEFAULT_LOGIN_IP_LIMIT ) ); + } + + /** + * Get the maximum login failures allowed per identifier. + * + * @return int + */ + private function get_login_identifier_limit() { + return max( 1, (int) apply_filters( 'rtmedia_api_login_identifier_limit', self::DEFAULT_LOGIN_IDENTIFIER_LIMIT ) ); + } + + /** + * Get the maximum invalid token submissions allowed per IP address. + * + * @return int + */ + private function get_token_limit() { + return max( 1, (int) apply_filters( 'rtmedia_api_token_attempt_limit', self::DEFAULT_TOKEN_ATTEMPT_LIMIT ) ); + } +} diff --git a/app/main/controllers/api/RTMediaJsonApi.php b/app/main/controllers/api/RTMediaJsonApi.php index 769dd1453..c7ea38509 100644 --- a/app/main/controllers/api/RTMediaJsonApi.php +++ b/app/main/controllers/api/RTMediaJsonApi.php @@ -137,6 +137,20 @@ class RTMediaJsonApi { */ public $msg_api_disabled = 'API disabled by site administrator'; + /** + * Error code for rate-limited API requests. + * + * @var int + */ + public $ec_rate_limit_exceeded = 600010; + + /** + * Error message for rate-limited API requests. + * + * @var string + */ + public $msg_rate_limit_exceeded = 'too many requests; please try again later'; + /** * Object of RTMediaJsonApiFunctions class to handle api requests. * @@ -306,33 +320,38 @@ public function rtmedia_api_process_wp_login_request() { $ec_user_pass_missing = 200001; $msg_user_pass_missing = esc_html__( 'username/password empty', 'buddypress-media' ); - $ec_incorrect_username = 200002; - $msg_incorrect_username = esc_html__( 'incorrect username', 'buddypress-media' ); + $ec_invalid_credentials = 200003; + $msg_invalid_credentials = esc_html__( 'invalid username or password', 'buddypress-media' ); - $ec_incorrect_pass = 200003; - $msg_incorrect_pass = esc_html__( 'incorrect password', 'buddypress-media' ); + $ec_login_rate_limited = 200006; + $msg_login_rate_limited = esc_html__( 'too many login attempts; please try again later', 'buddypress-media' ); $ec_login_success = 200004; $msg_login_success = esc_html__( 'login success', 'buddypress-media' ); - $username = sanitize_text_field( filter_input( INPUT_POST, 'username', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); - $password = sanitize_text_field( filter_input( INPUT_POST, 'password', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); + $username = filter_input( INPUT_POST, 'username', FILTER_UNSAFE_RAW ); + $password = filter_input( INPUT_POST, 'password', FILTER_UNSAFE_RAW ); + $username = is_string( $username ) ? sanitize_text_field( $username ) : ''; + $password = is_string( $password ) ? $password : ''; if ( empty( $username ) || empty( $password ) ) { wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $ec_user_pass_missing, $msg_user_pass_missing ) ); } else { - $user_login = wp_authenticate( trim( $username ), trim( $password ) ); + $rate_limiter = new RTMediaApiRateLimiter(); + $retry_after = $rate_limiter->get_login_retry_after( $username ); - if ( is_wp_error( $user_login ) ) { + if ( 0 < $retry_after ) { + header( 'Retry-After: ' . $retry_after ); + status_header( 429 ); + wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $ec_login_rate_limited, $msg_login_rate_limited ) ); + } - $incorrect_password = ! empty( $user_login->errors['incorrect_password'] ) ? true : false; - $incorrect_username = ! empty( $user_login->errors['invalid_username'] ) ? true : false; + $user_login = wp_authenticate( trim( $username ), $password ); - if ( $incorrect_password ) { - wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $ec_incorrect_pass, $msg_incorrect_pass ) ); - } elseif ( $incorrect_username ) { - wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $ec_incorrect_username, $msg_incorrect_username ) ); - } + if ( is_wp_error( $user_login ) ) { + $rate_limiter->record_login_failure( $username ); + wp_send_json( $this->rtmedia_api_response_object( 'FALSE', $ec_invalid_credentials, $msg_invalid_credentials ) ); } else { + $rate_limiter->clear_login_identifier( $username ); $access_token = $this->rtmediajsonapifunction->rtmedia_api_get_user_token( $user_login->ID, $user_login->data->user_login ); $data = array( @@ -347,7 +366,7 @@ public function rtmedia_api_process_wp_login_request() { 'user_id' => intval( $user_login->ID ), 'ip' => $remote_addr, 'token' => sanitize_text_field( $access_token ), - 'token_time' => date( 'Y-m-d H:i:s' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + 'token_time' => time(), ); $rtmapilogin->insert( $login_details ); wp_send_json( $this->rtmedia_api_response_object( 'TRUE', $ec_login_success, $msg_login_success, $data ) ); diff --git a/app/main/controllers/api/RTMediaJsonApiFunctions.php b/app/main/controllers/api/RTMediaJsonApiFunctions.php index 14d683343..152816258 100644 --- a/app/main/controllers/api/RTMediaJsonApiFunctions.php +++ b/app/main/controllers/api/RTMediaJsonApiFunctions.php @@ -11,6 +11,13 @@ */ class RTMediaJsonApiFunctions { + /** + * Token lifetime in seconds. + * + * @var int + */ + public $token_lifetime = 2592000; // 30 days. + /** * RTMediaJsonApiFunctions constructor. */ @@ -28,9 +35,20 @@ public function rtmedia_api_get_user_token( $user_id, $user_login ) { if ( empty( $user_id ) || empty( $user_login ) ) { return false; } - $string = '08~' . $user_id . '~' . $user_login . '~kumar'; - return sha1( $string . current_time( 'timestamp' ) . wp_rand( 1, 9 ) ); + // Generate an opaque 64 character long token for user login. + return wp_generate_password( 64, false, false ); + } + + /** + * Get the absolute API token lifetime in seconds. + * + * @return int + */ + public function rtmedia_api_get_token_lifetime() { + $token_lifetime = (int) apply_filters( 'rtmedia_api_token_lifetime', $this->token_lifetime ); + + return max( MINUTE_IN_SECONDS, $token_lifetime ); } /** @@ -96,6 +114,11 @@ public function rtmedia_api_validate_token( $token ) { return false; } + // Revoke predictable 40-character SHA-1 tokens issued by affected versions. + if ( 1 !== preg_match( '/^[A-Za-z0-9]{64}$/D', $token ) ) { + return false; + } + if ( class_exists( 'RTMediaApiLogin' ) ) { $rtmediaapilogin = new RTMediaApiLogin(); $columns = array( @@ -106,6 +129,15 @@ public function rtmedia_api_validate_token( $token ) { return false; } + $issued_at = isset( $token_data[0]->token_time ) ? (int) $token_data[0]->token_time : 0; + $expires_at = $issued_at + $this->rtmedia_api_get_token_lifetime(); + + if ( $issued_at <= 0 || time() >= $expires_at ) { + $rtmediaapilogin->update( array( 'status' => 'FALSE' ), array( 'id' => $token_data[0]->id ) ); + + return false; + } + return $token_data; } else { return false; @@ -124,6 +156,9 @@ public function rtmedia_api_get_user_id_from_token( $token ) { return false; } $token_data = $this->rtmedia_api_validate_token( $token ); + if ( empty( $token_data ) ) { + return false; + } return $token_data[0]->user_id; } @@ -132,17 +167,27 @@ public function rtmedia_api_get_user_id_from_token( $token ) { * Token processing for all data fetch/post requests */ public function rtmedia_api_verfiy_token() { - $rtmjsonapi = new RTMediaJsonApi(); - $token = sanitize_text_field( filter_input( INPUT_POST, 'token', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); + $rtmjsonapi = new RTMediaJsonApi(); + $rate_limiter = new RTMediaApiRateLimiter(); + $token = sanitize_text_field( filter_input( INPUT_POST, 'token', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); if ( empty( $token ) ) { wp_send_json( $rtmjsonapi->rtmedia_api_response_object( 'FALSE', $rtmjsonapi->ec_token_missing, $rtmjsonapi->msg_token_missing ) ); } + $retry_after = $rate_limiter->get_token_retry_after(); + + if ( 0 < $retry_after ) { + header( 'Retry-After: ' . $retry_after ); + status_header( 429 ); + wp_send_json( $rtmjsonapi->rtmedia_api_response_object( 'FALSE', $rtmjsonapi->ec_rate_limit_exceeded, $rtmjsonapi->msg_rate_limit_exceeded ) ); + } + // Validate token. $token_valid = $this->rtmedia_api_validate_token( $token ); if ( ! $token_valid ) { + $rate_limiter->record_token_failure(); wp_send_json( $rtmjsonapi->rtmedia_api_response_object( 'FALSE', $rtmjsonapi->ec_token_invalid, $rtmjsonapi->msg_token_invalid ) ); } } diff --git a/changelog.txt b/changelog.txt index d1d0ccbbf..3bc0adb40 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,13 @@ == Changelog == += 4.7.13 [August 27, 2026] = + +* FIXED + * Fixed insecure API token generation, validation and expiration. + * Fixed insufficient brute-force protection for API authentication and token validation. + * Fixed username enumeration through API login error responses. + + = 4.7.12 [August 18, 2026] = * FIXED diff --git a/index.php b/index.php index f85316e0f..1ac75c42c 100644 --- a/index.php +++ b/index.php @@ -3,7 +3,7 @@ * Plugin Name: rtMedia for WordPress, BuddyPress and bbPress * Plugin URI: https://rtmedia.io/?utm_source=dashboard&utm_medium=plugin&utm_campaign=buddypress-media * Description: This plugin adds missing media rich features like photos, videos and audio uploading to BuddyPress which are essential if you are building social network, seriously! - * Version: 4.7.12 + * Version: 4.7.13 * Requires at least: 4.9.6 * Text Domain: buddypress-media * Author: rtCamp @@ -26,7 +26,7 @@ /** * The version of the plugin */ - define( 'RTMEDIA_VERSION', '4.7.12' ); + define( 'RTMEDIA_VERSION', '4.7.13' ); } if ( ! defined( 'RTMEDIA_PATH' ) ) { diff --git a/languages/buddpress-media.pot b/languages/buddpress-media.pot index 099730dca..91eb52c51 100644 --- a/languages/buddpress-media.pot +++ b/languages/buddpress-media.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: rtMedia for WordPress, BuddyPress and bbPress 4.7.12\n" +"Project-Id-Version: rtMedia for WordPress, BuddyPress and bbPress 4.7.13\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/buddypress-media\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-08-17T13:32:53+00:00\n" +"POT-Creation-Date: 2026-08-27T11:14:49+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: buddypress-media\n" @@ -2261,217 +2261,217 @@ msgstr "" msgid "%1$s commented on %2$s's %3$s" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:307 +#: app/main/controllers/api/RTMediaJsonApi.php:321 msgid "username/password empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:310 -msgid "incorrect username" +#: app/main/controllers/api/RTMediaJsonApi.php:324 +msgid "invalid username or password" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:313 -msgid "incorrect password" +#: app/main/controllers/api/RTMediaJsonApi.php:327 +msgid "too many login attempts; please try again later" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:316 +#: app/main/controllers/api/RTMediaJsonApi.php:330 msgid "login success" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:365 +#: app/main/controllers/api/RTMediaJsonApi.php:384 msgid "fields empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:368 +#: app/main/controllers/api/RTMediaJsonApi.php:387 msgid "invalid email" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:371 +#: app/main/controllers/api/RTMediaJsonApi.php:390 msgid "password do not match" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:374 +#: app/main/controllers/api/RTMediaJsonApi.php:393 msgid "username already registered" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:377 +#: app/main/controllers/api/RTMediaJsonApi.php:396 msgid "email already exists" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:380 +#: app/main/controllers/api/RTMediaJsonApi.php:399 msgid "new user created" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:383 +#: app/main/controllers/api/RTMediaJsonApi.php:402 msgid "user registration is not allowed" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:457 +#: app/main/controllers/api/RTMediaJsonApi.php:476 msgid "email empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:460 +#: app/main/controllers/api/RTMediaJsonApi.php:479 msgid "username/email not registered" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:463 +#: app/main/controllers/api/RTMediaJsonApi.php:482 msgid "reset link sent" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:497 +#: app/main/controllers/api/RTMediaJsonApi.php:516 msgid "Someone has asked to reset the password for the following site and username." msgstr "" #. translators: 1: Username. -#: app/main/controllers/api/RTMediaJsonApi.php:501 +#: app/main/controllers/api/RTMediaJsonApi.php:520 #, php-format msgid "Username: %s" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:502 +#: app/main/controllers/api/RTMediaJsonApi.php:521 msgid "To reset your password visit the following address, otherwise just ignore this email and nothing will happen." msgstr "" #. translators: 1: Blog name. -#: app/main/controllers/api/RTMediaJsonApi.php:507 +#: app/main/controllers/api/RTMediaJsonApi.php:526 #, php-format msgid "[%s] Password Reset" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:523 +#: app/main/controllers/api/RTMediaJsonApi.php:542 msgid "bp activities" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:526 +#: app/main/controllers/api/RTMediaJsonApi.php:545 msgid "user activities" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:535 +#: app/main/controllers/api/RTMediaJsonApi.php:554 msgid "no updates" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:558 +#: app/main/controllers/api/RTMediaJsonApi.php:577 msgid "comment content missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:561 +#: app/main/controllers/api/RTMediaJsonApi.php:580 msgid "comment posted" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:609 +#: app/main/controllers/api/RTMediaJsonApi.php:628 msgid "unliked media" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:612 +#: app/main/controllers/api/RTMediaJsonApi.php:631 msgid "liked media" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:704 +#: app/main/controllers/api/RTMediaJsonApi.php:723 msgid "no comments" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:707 +#: app/main/controllers/api/RTMediaJsonApi.php:726 msgid "media comments" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:771 +#: app/main/controllers/api/RTMediaJsonApi.php:790 msgid "no likes" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:774 +#: app/main/controllers/api/RTMediaJsonApi.php:793 msgid "media likes" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:835 +#: app/main/controllers/api/RTMediaJsonApi.php:854 msgid "invalid comment/media id" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:838 +#: app/main/controllers/api/RTMediaJsonApi.php:857 msgid "no comment id" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:841 +#: app/main/controllers/api/RTMediaJsonApi.php:860 msgid "comment deleted" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:945 +#: app/main/controllers/api/RTMediaJsonApi.php:964 msgid "no profile found" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:948 +#: app/main/controllers/api/RTMediaJsonApi.php:967 msgid "profile fields" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1050 +#: app/main/controllers/api/RTMediaJsonApi.php:1069 msgid "follow user id missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1053 +#: app/main/controllers/api/RTMediaJsonApi.php:1072 msgid "started following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1056 +#: app/main/controllers/api/RTMediaJsonApi.php:1075 msgid "already following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1089 +#: app/main/controllers/api/RTMediaJsonApi.php:1108 msgid "unfollow id missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1092 +#: app/main/controllers/api/RTMediaJsonApi.php:1111 msgid "stopped following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1095 +#: app/main/controllers/api/RTMediaJsonApi.php:1114 msgid "not following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1128 +#: app/main/controllers/api/RTMediaJsonApi.php:1147 msgid "name/location empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1131 +#: app/main/controllers/api/RTMediaJsonApi.php:1150 msgid "profile updated" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1160 -#: app/main/controllers/api/RTMediaJsonApi.php:1188 +#: app/main/controllers/api/RTMediaJsonApi.php:1179 +#: app/main/controllers/api/RTMediaJsonApi.php:1207 msgid "no file" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1163 -#: app/main/controllers/api/RTMediaJsonApi.php:1200 +#: app/main/controllers/api/RTMediaJsonApi.php:1182 +#: app/main/controllers/api/RTMediaJsonApi.php:1219 msgid "upload failed, check size and file type" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1166 +#: app/main/controllers/api/RTMediaJsonApi.php:1185 msgid "avatar updated" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1191 +#: app/main/controllers/api/RTMediaJsonApi.php:1210 msgid "invalid file type. jpeg and png are allowed." msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1194 +#: app/main/controllers/api/RTMediaJsonApi.php:1213 msgid "image type missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1197 +#: app/main/controllers/api/RTMediaJsonApi.php:1216 msgid "no title" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1203 +#: app/main/controllers/api/RTMediaJsonApi.php:1222 msgid "media updated" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1417 +#: app/main/controllers/api/RTMediaJsonApi.php:1436 msgid "media list" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1420 +#: app/main/controllers/api/RTMediaJsonApi.php:1439 msgid "no media found for requested media type" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1538 +#: app/main/controllers/api/RTMediaJsonApi.php:1557 msgid "single media" msgstr "" diff --git a/languages/buddypress-media.pot b/languages/buddypress-media.pot index 584709b10..4aa99a7b7 100644 --- a/languages/buddypress-media.pot +++ b/languages/buddypress-media.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: rtMedia for WordPress, BuddyPress and bbPress 4.7.12\n" +"Project-Id-Version: rtMedia for WordPress, BuddyPress and bbPress 4.7.13\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/buddypress-media\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-08-17T19:10:32+00:00\n" +"POT-Creation-Date: 2026-08-27T11:14:44+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: buddypress-media\n" @@ -2261,217 +2261,217 @@ msgstr "" msgid "%1$s commented on %2$s's %3$s" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:307 +#: app/main/controllers/api/RTMediaJsonApi.php:321 msgid "username/password empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:310 -msgid "incorrect username" +#: app/main/controllers/api/RTMediaJsonApi.php:324 +msgid "invalid username or password" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:313 -msgid "incorrect password" +#: app/main/controllers/api/RTMediaJsonApi.php:327 +msgid "too many login attempts; please try again later" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:316 +#: app/main/controllers/api/RTMediaJsonApi.php:330 msgid "login success" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:365 +#: app/main/controllers/api/RTMediaJsonApi.php:384 msgid "fields empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:368 +#: app/main/controllers/api/RTMediaJsonApi.php:387 msgid "invalid email" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:371 +#: app/main/controllers/api/RTMediaJsonApi.php:390 msgid "password do not match" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:374 +#: app/main/controllers/api/RTMediaJsonApi.php:393 msgid "username already registered" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:377 +#: app/main/controllers/api/RTMediaJsonApi.php:396 msgid "email already exists" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:380 +#: app/main/controllers/api/RTMediaJsonApi.php:399 msgid "new user created" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:383 +#: app/main/controllers/api/RTMediaJsonApi.php:402 msgid "user registration is not allowed" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:457 +#: app/main/controllers/api/RTMediaJsonApi.php:476 msgid "email empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:460 +#: app/main/controllers/api/RTMediaJsonApi.php:479 msgid "username/email not registered" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:463 +#: app/main/controllers/api/RTMediaJsonApi.php:482 msgid "reset link sent" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:497 +#: app/main/controllers/api/RTMediaJsonApi.php:516 msgid "Someone has asked to reset the password for the following site and username." msgstr "" #. translators: 1: Username. -#: app/main/controllers/api/RTMediaJsonApi.php:501 +#: app/main/controllers/api/RTMediaJsonApi.php:520 #, php-format msgid "Username: %s" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:502 +#: app/main/controllers/api/RTMediaJsonApi.php:521 msgid "To reset your password visit the following address, otherwise just ignore this email and nothing will happen." msgstr "" #. translators: 1: Blog name. -#: app/main/controllers/api/RTMediaJsonApi.php:507 +#: app/main/controllers/api/RTMediaJsonApi.php:526 #, php-format msgid "[%s] Password Reset" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:523 +#: app/main/controllers/api/RTMediaJsonApi.php:542 msgid "bp activities" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:526 +#: app/main/controllers/api/RTMediaJsonApi.php:545 msgid "user activities" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:535 +#: app/main/controllers/api/RTMediaJsonApi.php:554 msgid "no updates" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:558 +#: app/main/controllers/api/RTMediaJsonApi.php:577 msgid "comment content missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:561 +#: app/main/controllers/api/RTMediaJsonApi.php:580 msgid "comment posted" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:609 +#: app/main/controllers/api/RTMediaJsonApi.php:628 msgid "unliked media" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:612 +#: app/main/controllers/api/RTMediaJsonApi.php:631 msgid "liked media" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:704 +#: app/main/controllers/api/RTMediaJsonApi.php:723 msgid "no comments" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:707 +#: app/main/controllers/api/RTMediaJsonApi.php:726 msgid "media comments" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:771 +#: app/main/controllers/api/RTMediaJsonApi.php:790 msgid "no likes" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:774 +#: app/main/controllers/api/RTMediaJsonApi.php:793 msgid "media likes" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:835 +#: app/main/controllers/api/RTMediaJsonApi.php:854 msgid "invalid comment/media id" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:838 +#: app/main/controllers/api/RTMediaJsonApi.php:857 msgid "no comment id" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:841 +#: app/main/controllers/api/RTMediaJsonApi.php:860 msgid "comment deleted" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:945 +#: app/main/controllers/api/RTMediaJsonApi.php:964 msgid "no profile found" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:948 +#: app/main/controllers/api/RTMediaJsonApi.php:967 msgid "profile fields" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1050 +#: app/main/controllers/api/RTMediaJsonApi.php:1069 msgid "follow user id missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1053 +#: app/main/controllers/api/RTMediaJsonApi.php:1072 msgid "started following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1056 +#: app/main/controllers/api/RTMediaJsonApi.php:1075 msgid "already following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1089 +#: app/main/controllers/api/RTMediaJsonApi.php:1108 msgid "unfollow id missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1092 +#: app/main/controllers/api/RTMediaJsonApi.php:1111 msgid "stopped following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1095 +#: app/main/controllers/api/RTMediaJsonApi.php:1114 msgid "not following" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1128 +#: app/main/controllers/api/RTMediaJsonApi.php:1147 msgid "name/location empty" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1131 +#: app/main/controllers/api/RTMediaJsonApi.php:1150 msgid "profile updated" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1160 -#: app/main/controllers/api/RTMediaJsonApi.php:1188 +#: app/main/controllers/api/RTMediaJsonApi.php:1179 +#: app/main/controllers/api/RTMediaJsonApi.php:1207 msgid "no file" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1163 -#: app/main/controllers/api/RTMediaJsonApi.php:1200 +#: app/main/controllers/api/RTMediaJsonApi.php:1182 +#: app/main/controllers/api/RTMediaJsonApi.php:1219 msgid "upload failed, check size and file type" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1166 +#: app/main/controllers/api/RTMediaJsonApi.php:1185 msgid "avatar updated" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1191 +#: app/main/controllers/api/RTMediaJsonApi.php:1210 msgid "invalid file type. jpeg and png are allowed." msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1194 +#: app/main/controllers/api/RTMediaJsonApi.php:1213 msgid "image type missing" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1197 +#: app/main/controllers/api/RTMediaJsonApi.php:1216 msgid "no title" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1203 +#: app/main/controllers/api/RTMediaJsonApi.php:1222 msgid "media updated" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1417 +#: app/main/controllers/api/RTMediaJsonApi.php:1436 msgid "media list" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1420 +#: app/main/controllers/api/RTMediaJsonApi.php:1439 msgid "no media found for requested media type" msgstr "" -#: app/main/controllers/api/RTMediaJsonApi.php:1538 +#: app/main/controllers/api/RTMediaJsonApi.php:1557 msgid "single media" msgstr "" diff --git a/readme.txt b/readme.txt index 6acad4f03..f62f0122e 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === rtMedia for WordPress, BuddyPress and bbPress === -Contributors: rtcamp, mangeshp, sanket.parmar, pranalipatel, jignesh.nakrani, manishsongirkar36, kiranpotphode, yahil, 1naveengiri, bhargavbhandari90, raftaar1191, rittesh.patel, sagarjadhav, pushpak.pop, faishal, desaiuditd, rahul286, JoshuaAbenazer, gagan0123, saurabhshukla, nitun.lanjewar, umesh.nevase, suhasgirgaonkar, neerukoul, hrishiv90, kanakiyajay, jarretc, tobiaskluge, rafaelfunchal, UmeshSingla, mehulkaklotar, tannermirabel, kishores, chandrapatel, rahul3883, nomnom99, sayanchakraborty, milindmore22, thrijith, abhijitrakas, sid177, montu3366, jashwini, juhise, ravatparmar, dharmin16, malavvasita, pooja1210, krupajnanda, surajkumarsingh, kanumalivad, dishitpala, shobhit2412, vaishu.agola27, kapilpaul, opurockey, vkd007, pavanpatil1, pradeep1308, shardul200, sabbir1991, kamalahmed, ibnulk, harshbarach, Mukulsingh27, vishalkakadiya, elifvish, krupajnanda, utsavladani, krishana79, rohitmathur7, kuldipchaudhary, mchirag2002, vedantgandhi28, mohamedahamed, the-hercules +Contributors: rtcamp, mangeshp, sanket.parmar, pranalipatel, jignesh.nakrani, manishsongirkar36, kiranpotphode, yahil, 1naveengiri, bhargavbhandari90, raftaar1191, rittesh.patel, sagarjadhav, pushpak.pop, faishal, desaiuditd, rahul286, JoshuaAbenazer, gagan0123, saurabhshukla, nitun.lanjewar, umesh.nevase, suhasgirgaonkar, neerukoul, hrishiv90, kanakiyajay, jarretc, tobiaskluge, rafaelfunchal, UmeshSingla, mehulkaklotar, tannermirabel, kishores, chandrapatel, rahul3883, nomnom99, sayanchakraborty, milindmore22, thrijith, abhijitrakas, sid177, montu3366, jashwini, juhise, ravatparmar, dharmin16, malavvasita, pooja1210, krupajnanda, surajkumarsingh, kanumalivad, dishitpala, shobhit2412, vaishu.agola27, kapilpaul, opurockey, vkd007, pavanpatil1, pradeep1308, shardul200, sabbir1991, kamalahmed, ibnulk, harshbarach, Mukulsingh27, vishalkakadiya, elifvish, krupajnanda, utsavladani, krishana79, rohitmathur7, kuldipchaudhary, mchirag2002, vedantgandhi28, mohamedahamed, thehercules, suryakantupadhyay Tags: BuddyPress, media, multimedia, album, audio, music, video, photo, upload, share, MediaElement.js, media-node, rtMedia, WordPress, bbPress, masonry License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Requires at least: 4.9.6 Tested up to: 7.0 -Stable tag: 4.7.12 +Stable tag: 4.7.13 Add albums, photo, audio/video upload, privacy, sharing, front-end uploads & more. All this works on mobile/tablets devices. @@ -128,6 +128,13 @@ http://www.youtube.com/watch?v=dJrykKQGDcs == Changelog == += 4.7.13 [August 27, 2026] = + +* FIXED + * Fixed insecure API token generation, validation and expiration. + * Fixed insufficient brute-force protection for API authentication and token validation. + * Fixed username enumeration through API login error responses. + = 4.7.12 [August 18, 2026] = * FIXED @@ -1017,6 +1024,9 @@ http://www.youtube.com/watch?v=dJrykKQGDcs == Upgrade Notice == += 4.7.13 = +This release includes important security improvements for API authentication and tokens. Existing API users must log in again to receive a new token. We strongly recommend upgrading. + = 4.7.12 = This release includes important security fixes and permission improvements. We strongly recommend upgrading.