missing_capability
wp_ajax_wllp_save_settings is registered from Route::loadAdminHooks and implemented by Controller::saveSettings. After wp_verify_nonce($input->post('nonce'), WLLP_PLUGIN_SLUG) the handler parse_str()s the posted form blob and update_option('wllp_settings_data', $settings) with no current_user_can() and no allowlist of keys/values. The settings menu is only registered when current_user_can('manage_options'), so shop_manager/editor/author/contributor are not supposed to change this add-on. They still can: loadAssets() is hooked to admin_enqueue_scripts and was meant to localize the nonce only on page=wllp-point-based-level, but the condition is `empty($_GET['page'] || $_GET['page'] != WLLP_PLUGIN_SLUG)` which is never true once ?page= is present, so wllp_localize_data.nonce is printed on every WooCommerce admin screen (admin.php?page=wc-admin, wc-orders, …) and on profile.php?page=… . A shop_manager or contributor copies that nonce and POSTs action=wllp_save_settings&data=levels_from_which_point_based=from_current_balance (or from_order_total, which is commented out of the UI select but accepted because the value is not allowlisted). Observable: option wllp_settings_data changes; every customer's WPLoyalty level is recomputed from the new source; from_order_total additionally turns on getOrderTotal() SQL (separate finding). Default WooCommerce customer/subscriber roles are redirected out of wp-admin before enqueue, so they do not get this nonce unless woocommerce_prevent_admin_access is disabled. Nonce is not a capability check.
App/Controllers/Controller.php:93-128,176-218
RecommendationRequire current_user_can('manage_options') (the same gate as addMenu) immediately after the nonce check, and fail closed. Fix the page-slug test so the nonce is only localized on this plugin's screen. Allowlist keys (levels_from_which_point_based, order_duration, grace_period_enabled, grace_period_days) and values before update_option; do not persist the raw parse_str array.