Skip to content

Commit a91f5da

Browse files
committed
Final version bump to v5.2.2 for public release.
Squashed commit of the following: commit 6a98399 Author: Prajjwal Poudel <85762481+iamprazol@users.noreply.github.com> Date: Fri Jun 19 09:22:11 2026 +0545 Fix/vulnerability v5.2.1 (#1300) * UR-4561 Fix - Subscriber-Level IDOR and Payment Bypass on Membership Upgrade Action * Fix - Unauthenticated Privilege Escalation via Off-Form Membership Tier Substitution in Registration * Fix - Webhook not configured but still can bypass paypal payments * Fix - Delete intercepeted attacker registered as user * Fix - Issue * Sync codes to pro * UR-4584 Fix - PayPal Upgrade Membership – Incorrect Redirect & Incomplete Upgrade Action commit 5b11d71 Author: Prajjwal Poudel <iamprazol@gmail.com> Date: Fri Jun 19 09:20:20 2026 +0545 UR-4586 Fix - Hardcoded database table names causing failures on custom table prefixes commit e810837 Author: Prajjwal Poudel <85762481+iamprazol@users.noreply.github.com> Date: Fri Jun 19 09:09:10 2026 +0545 Fix - Password reset link shows invalid or expired error after update from 5.1.6 (#1299) commit 9d0462d Author: saurav <133834846+saurab018@users.noreply.github.com> Date: Thu Jun 18 15:44:11 2026 +0545 UR-4576: Fix membership subscriptions stuck pending on 3D Secure cards (#1303) Stripe SDK (Basil API) dropped invoice.payment_intent in favour of invoice.payments; the front-end still read payment_intent and crashed, so the 3DS challenge never ran and orders/subscriptions stayed pending. Resolve the PaymentIntent server-side and expose it for SCA; guard the client handler and confirm 3D Secure. commit 7bb1e64 Author: saurav <133834846+saurab018@users.noreply.github.com> Date: Thu Jun 18 15:39:29 2026 +0545 UR-4547 Fix - Checkbox visibility issue caused by CSS conflict from User Registration & Membership plugin (admin.css) (#1295) The plugin's admin.css applies to checkboxes/radios on the WP user profile page with higher specificity than WP core's . WP draws the checkmark as a white SVG via , so a checked box shows a white checkmark on a white background — appearing empty. Adds a scoped, additive rule that restores the theme-color background only on the WP profile/user-edit pages so the white check becomes visible. No existing rules are modified. commit 031878e Author: saurav <133834846+saurab018@users.noreply.github.com> Date: Thu Jun 18 15:38:14 2026 +0545 UR-4550 Fix - Password reset link shows invalid or expired on hosts that strip the reset cookie (#1296) * UR-4550 Fix - Password reset link shows invalid or expired on hosts that strip the reset cookie The reset flow is a two-step cookie handoff: redirect_reset_password_link() sets the wp-resetpass-* cookie then 302s to the form, which reads the cookie back to validate the key. Managed hosts/CDNs (e.g. Flywheel behind Fastly) strip the non-allowlisted Set-Cookie at the edge, so the form never sees the cookie and every user gets 'invalid or expired' regardless of key validity. Add a cookie-independent fallback: stash login:key in a short-lived transient keyed by an opaque token carried through the redirect, and repopulate from it at step 2 only when the cookie is missing. Existing code runs unchanged and the cookie stays the primary path (no-op on hosts that deliver it). Token is a 32-char CSPRNG value, single-use (deleted on reset), 1h TTL, alnum-validated, with Referrer-Policy: no-referrer so it cannot leak via the Referer header. * comment managed commit 1b24a0e Author: saurav <133834846+saurab018@users.noreply.github.com> Date: Thu Jun 18 15:34:39 2026 +0545 UR-4326 Dev - Add UR_WPML compatibility service class for centralized WPML handling (#1301) commit 7a618af Author: saurav <133834846+saurab018@users.noreply.github.com> Date: Thu Jun 18 14:37:00 2026 +0545 Fix: PayPal production credentials not detected due to option key mismatch (#1302) Credentials are saved under option keys but were looked up using the raw mode value , causing errors when production mode was active. Added helper that maps → for option key lookups while leaving the mode value unchanged for API URL and flow comparisons. Fixes UR-4565. commit 1787bee Author: Prajjwal Poudel <iamprazol@gmail.com> Date: Wed Jun 10 09:10:10 2026 +0545 UR-4556 Fix - Payment Method Bypass Allows Free Access to Paid Memberships via Tampered Registration Request commit dc79fbf Author: Prajjwal Poudel <iamprazol@gmail.com> Date: Tue Jun 9 15:57:21 2026 +0545 UR-4555 Fix - Missing Paypal Webhook Signature Verification.
1 parent 50eddbb commit a91f5da

21 files changed

Lines changed: 1389 additions & 887 deletions

CHANGELOG.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
= 5.2.2 - 19/06/2026 =
2+
* Dev - Add UR_WPML compatibility service class.
3+
* Fix - Password reset link shows "invalid or expired" error.
4+
* Fix - Membership subscriptions stuck pending on 3D Secure (SCA) cards.
5+
* Fix - Hardcoded database table names causing failures on custom table prefixes.
6+
* Fix - Checkbox visibility issue on WP user profile/edit page caused by CSS conflict.
7+
* Fix - PayPal credentials not detected during registration despite valid saved settings.
8+
* Fix - Password reset link shows invalid or expired on hosts that do not support PHP cookies.
9+
* Fix - Webhook not configured but still can bypass PayPal payments.
10+
* Fix - Payment Method Bypass Allows Free Access to Paid Memberships via Tampered Registration Request.
11+
* Fix - Payment method field during registration could be manipulated to bypass payment for paid memberships.
12+
* Fix - Authenticated users could modify other members' subscriptions by supplying an arbitrary subscription ID during upgrade.
13+
* Fix - Membership tier submitted during registration was not validated against the memberships configured on the form, allowing substitution with an off-form tier.
14+
115
= 5.2.1 - 28/05/2026 =
216
* Fix - Stripe order validation issue.
317
* Fix - Membership upgrade action not available after disabling group add-on.

assets/css/admin-rtl.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/css/admin.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/css/admin.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8703,6 +8703,24 @@ body.user-registration-page {
87038703
}
87048704
}
87058705

8706+
// Additive bugfix: the checkbox/radio rule above sets background:#fff with higher specificity than WP core's
8707+
// input:checked{background:theme-color}, so a checked box stays white and WP's white checkmark SVG is invisible.
8708+
// Restore the theme-color background on :checked only (scope = WP profile/user-edit pages) so the check shows.
8709+
body.user-registration-page {
8710+
#profile-page {
8711+
#your-profile {
8712+
table.form-table,
8713+
#application-passwords-section {
8714+
input[type="checkbox"]:checked,
8715+
input[type="radio"]:checked {
8716+
background: var(--wp-admin-theme-color, #2271b1);
8717+
border-color: var(--wp-admin-theme-color, #2271b1);
8718+
}
8719+
}
8720+
}
8721+
}
8722+
}
8723+
87068724
.ur-sticky-wrapper {
87078725
top: 0;
87088726
background: $white;

assets/js/frontend/user-registration-recaptcha.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/js/modules/membership/frontend/user-registration-membership-frontend.js

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2518,59 +2518,85 @@
25182518
ur_membership_frontend_utils.show_payment_processing_overlay();
25192519

25202520
return new Promise(function (resolve, reject) {
2521+
var subscription = data.subscription || {};
2522+
var resolvePayload = {
2523+
subscription: subscription,
2524+
response_data: data.response_data,
2525+
message: data.message,
2526+
prepare_members_data: data.prepare_members_data,
2527+
form_response: data.form_response
2528+
};
2529+
25212530
if (
2522-
data.subscription &&
2523-
(data.subscription.status === "active" ||
2524-
data.subscription.status === "trialing")
2531+
subscription.status === "active" ||
2532+
subscription.status === "trialing"
25252533
) {
2526-
resolve({
2527-
subscription: data.subscription,
2528-
response_data: data.response_data,
2529-
message: data.message,
2530-
prepare_members_data: data.prepare_members_data,
2531-
form_response: data.form_response
2532-
});
25332534
ur_membership_frontend_utils.hide_payment_processing_overlay();
2535+
resolve(resolvePayload);
2536+
return;
25342537
}
25352538

2536-
var paymentIntent =
2537-
data.subscription.latest_invoice.payment_intent;
2538-
2539-
if ("trialing" !== data.subscription.status) {
2540-
if ("requires_action" === paymentIntent.status) {
2541-
data.paymentElements.stripe
2542-
.confirmCardPayment(paymentIntent.client_secret, {
2543-
payment_method: data.paymentMethodId
2544-
})
2545-
.then(function (result) {
2546-
if (result.error) {
2547-
var message = result.error.message;
2548-
reject(message, data);
2549-
return;
2550-
}
2539+
// Only an expanded PaymentIntent with a client_secret allows SCA / 3D Secure on the client.
2540+
var latestInvoice = subscription.latest_invoice || {};
2541+
var paymentIntent = latestInvoice.payment_intent;
2542+
var clientSecret =
2543+
paymentIntent && typeof paymentIntent === "object"
2544+
? paymentIntent.client_secret
2545+
: null;
2546+
var piStatus =
2547+
paymentIntent && typeof paymentIntent === "object"
2548+
? paymentIntent.status
2549+
: null;
2550+
var needsAction =
2551+
piStatus === "requires_action" ||
2552+
piStatus === "requires_source_action" ||
2553+
piStatus === "requires_confirmation";
2554+
2555+
if (clientSecret && needsAction) {
2556+
data.paymentElements.stripe
2557+
.confirmCardPayment(clientSecret)
2558+
.then(function (result) {
2559+
ur_membership_frontend_utils.hide_payment_processing_overlay();
2560+
if (result.error) {
2561+
reject(result.error.message, data);
2562+
return;
2563+
}
25512564

2552-
if (
2553-
"succeeded" === result.paymentIntent.status
2554-
) {
2555-
data.subscription.status = "active";
2556-
resolve({
2557-
subscription: data.subscription,
2558-
form_id: data.form_response.form_id,
2559-
response_data: data.response_data,
2560-
prepare_members_data:
2561-
data.prepare_members_data,
2562-
form_response: data.form_response,
2563-
three_d_secure: true
2564-
});
2565-
} else {
2566-
var message =
2567-
"Unable to complete the payment.";
2568-
reject(message, data);
2569-
}
2570-
ur_membership_frontend_utils.hide_payment_processing_overlay();
2571-
});
2572-
}
2565+
if (
2566+
result.paymentIntent &&
2567+
"succeeded" === result.paymentIntent.status
2568+
) {
2569+
subscription.status = "active";
2570+
resolve({
2571+
subscription: subscription,
2572+
form_id: data.form_response.form_id,
2573+
response_data: data.response_data,
2574+
prepare_members_data:
2575+
data.prepare_members_data,
2576+
form_response: data.form_response,
2577+
three_d_secure: true
2578+
});
2579+
} else {
2580+
reject(
2581+
"Unable to complete the payment.",
2582+
data
2583+
);
2584+
}
2585+
})
2586+
.catch(function (error) {
2587+
ur_membership_frontend_utils.hide_payment_processing_overlay();
2588+
reject(
2589+
error && error.message
2590+
? error.message
2591+
: "Unable to complete the payment.",
2592+
data
2593+
);
2594+
});
2595+
return;
25732596
}
2597+
2598+
ur_membership_frontend_utils.hide_payment_processing_overlay();
2599+
resolve(resolvePayload);
25742600
});
25752601
},
25762602

assets/js/modules/membership/frontend/user-registration-membership-frontend.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composer.lock

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/class-ur-form-handler.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public static function init() {
4242
*/
4343
public static function redirect_reset_password_link() {
4444
global $wp;
45+
46+
self::maybe_restore_reset_password_cookie();
47+
4548
if ( isset( $wp->query_vars['ur-lost-password'] ) && empty( $wp->query_vars['ur-lost-password'] ) ) {
4649
return;
4750
}
@@ -53,12 +56,68 @@ public static function redirect_reset_password_link() {
5356

5457
if ( ( $is_ur_lost_password_page || $is_ur_login_or_account_page ) && ! empty( $_GET['key'] ) && ! empty( $_GET['login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
5558
$value = sprintf( '%s:%s', sanitize_text_field( wp_unslash( $_GET['login'] ) ), sanitize_text_field( wp_unslash( $_GET['key'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
59+
60+
if ( ! headers_sent() ) {
61+
nocache_headers();
62+
}
63+
5664
UR_Shortcode_My_Account::set_reset_password_cookie( $value );
5765

58-
wp_safe_redirect( add_query_arg( 'show-reset-form', 'true', ur_resetpassword_url() ) );
66+
$redirect_url = add_query_arg( 'show-reset-form', 'true', ur_resetpassword_url() );
67+
68+
$token = wp_generate_password( 32, false );
69+
set_transient( 'ur_rp_' . $token, $value, HOUR_IN_SECONDS );
70+
$redirect_url = add_query_arg( 'urt', $token, $redirect_url );
71+
72+
wp_safe_redirect( $redirect_url );
5973
exit;
6074
}
6175
}
76+
77+
/**
78+
* Restore the reset-password cookie from a transient when a CDN strips it at the edge.
79+
*/
80+
private static function maybe_restore_reset_password_cookie() {
81+
$token = self::get_reset_password_handoff_token();
82+
83+
if ( '' === $token ) {
84+
return;
85+
}
86+
87+
// The opaque token lands in the URL, so keep it out of the Referer header.
88+
if ( ! headers_sent() ) {
89+
header( 'Referrer-Policy: no-referrer' );
90+
}
91+
92+
$rp_cookie = 'wp-resetpass-' . COOKIEHASH;
93+
94+
if ( ! empty( $_COOKIE[ $rp_cookie ] ) ) {
95+
return;
96+
}
97+
98+
$value = get_transient( 'ur_rp_' . $token );
99+
100+
if ( false !== $value ) {
101+
$_COOKIE[ $rp_cookie ] = $value;
102+
103+
set_transient( 'ur_rp_' . $token, $value, HOUR_IN_SECONDS );
104+
}
105+
}
106+
107+
/**
108+
* @return string Validated [A-Za-z0-9] token from the current request, or ''.
109+
*/
110+
private static function get_reset_password_handoff_token() {
111+
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only lookup; key is validated by check_password_reset_key().
112+
if ( empty( $_GET['show-reset-form'] ) || empty( $_GET['urt'] ) ) {
113+
return '';
114+
}
115+
116+
$token = sanitize_text_field( wp_unslash( $_GET['urt'] ) );
117+
// phpcs:enable WordPress.Security.NonceVerification.Recommended
118+
119+
return ctype_alnum( $token ) ? $token : '';
120+
}
62121
/**
63122
* Save and update a profie fields if the form was submitted through the user account page.
64123
*
@@ -789,6 +848,12 @@ public static function process_reset_password() {
789848
*/
790849
do_action( 'user_registration_reset_password', $user );
791850

851+
// Single-use: drop the CDN-proof transient handoff token now the reset succeeded.
852+
$handoff_token = self::get_reset_password_handoff_token();
853+
if ( '' !== $handoff_token ) {
854+
delete_transient( 'ur_rp_' . $handoff_token );
855+
}
856+
792857
$ur_account_page_exists = ur_get_page_id( 'myaccount' ) > 0;
793858
$ur_login_or_account_page = ur_get_page_permalink( 'myaccount' );
794859

0 commit comments

Comments
 (0)