WP Manifestindependent plugin directory
manifest / ecommerce / wp-loyalty-point-sharing

WPLoyalty - Point Sharing WP Registry grade D+. High-severity findings · 3 findingsWP RegistryHigh-severity findings · 3 findingsOpen the reportD+

★ 0stars
74release downloads
≈10active sites
stablelatest · 11mo ago

Install

Download wp-loyalty-point-sharing.zip From the stable release on GitHub, Nov 26, 2025

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/wployalty/wp-loyalty-point-sharing/releases/download/stable/wp-loyalty-point-sharing.zip
Updates itself. It ships its own updater (Plugin Update Checker), so new versions appear under Dashboard → Updates.

Releases

2 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
stable latest
Nov 26, 2025 11mo ago
wp-loyalty-point-sharing.zip
68
Nov 26, 2025 11mo ago
wp-loyalty-point-sharing-v1.0.0.zip
6

Active-site estimate ≈10 comes from the median of recent superseded releases. Method.

D+ grade

Security

WPLoyalty - Point Sharing 1.0.0 · audited by WP Registry

High-severity findings.

1 high 2 medium
Audited release
1.0.0
Findings
3
Worst severity
high
Content hash
d182e90905cbe91b308ff800…

Findings

  • high Non-atomic point-transfer confirmation lets customers mint loyalty points

    auth_bypass

    handleConfirmTransfer is hooked to init and completes a transfer from a GET link (?wlps_action=confirm_transfer&transfer_id=&token=). validateTransferToken only checks that the row is still pending; there is no SELECT … FOR UPDATE, no compare-and-swap on status, and status is not flipped to a terminal value until after performPointTransfer returns. Two parallel requests from the logged-in sender both observe pending, both pass validateSenderAndRecipient (both see the pre-debit balance), and both call performPointTransfer. That method always credits the full transfer amount: the credit addExtraPointAction runs even if debit returned false, and WPLoyalty's Base::addExtraPointAction (required parent plugin) clamps an oversized debit to the remaining balance instead of aborting, so a second debit of 0 still returns success while the credit of N points proceeds. A customer with 1 point can therefore issue many concurrent confirmation requests and grow the recipient WPLoyalty balance without a matching debit. Those points are store credit (WPLoyalty redeem-to-coupon). The 2/minute rate limit applies only to transfer initiation, not confirmation.

    App/Controller/PointTransferController.php:144-202

    Recommendation

    Consume the pending row atomically before moving points (UPDATE ... SET status='processing' WHERE id=%d AND status='pending' AND token=%s and require affected_rows=1). Run debit and credit in a single DB transaction and abort the credit if debit does not remove the full amount. Do not credit when debit returns false or when the parent helper clamped the debit to 0. Redirect to a token-free URL after handling the GET.

  • medium max_transfer_points is enforced only in JavaScript

    unsafe_input

    Admin settings store max_transfer_points in option wlps_settings and Common::addFrontendAssets localizes it for client-side checks in transfer-modal.js. transferPoints() casts POST transfer_points to int and Validation::validateTransferPointsInput only requires a valid recipient email and an integer >= 1. validateTransferRequest checks sender != recipient, email opt-in, recipient not banned, and sender balance, but never reads wlps_settings['max_transfer_points']. A logged-in customer (or anyone with that customer's nonce) can POST action=wlps_transfer_points with transfer_points equal to their full wallet, receive the confirmation email, and complete a transfer far above the merchant-configured cap. Combined with the confirmation race, the entire balance can be duplicated rather than capped.

    App/Helpers/Validation.php:38-57

    Recommendation

    In transferPoints and again in handleConfirmTransfer, load (int) get_option('wlps_settings')['max_transfer_points'] (or the plugin default) and reject any transfer_points above that cap. Do not rely on the localized JS check.

  • medium Shop Manager can write an arbitrary autoloaded option via saveSettings

    unsafe_input

    wp_ajax_wlps_save_settings requires manage_woocommerce and nonce wlps-setting-nonce (shop_manager and administrator; not manage_options). option_key is taken from POST, stripped to [A-Za-z0-9_-], and passed straight to update_option($key, $data, true) with autoload enabled. $data is the raw Input::post() array minus option_key/action/wlps_nonce; Validation::validateSettingsTab only requires max_transfer_points to be an integer >= 1 and enable_share_point to be yes/no if present — extra POST keys are not stripped. A shop_manager can therefore overwrite any matching wp_options key with an array payload (for example users_can_register as a truthy array, or active_plugins / wp_user_roles / template, which breaks the site). They should not be able to write arbitrary autoloaded options outside wlps_settings.

    App/Controller/Admin/AdminController.php:282-321

    Recommendation

    Hard-allowlist option_key to 'wlps_settings' (or Util::getDefaults('save_key')). Allowlist saved fields to enable_share_point and max_transfer_points, sanitize them, and call update_option only for that key. Require manage_options if arbitrary option names are ever needed.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.