[GH-ISSUE #5340] Improper callbackURL Parameter Encoding Throughout Codebase #27540

Closed
opened 2026-04-17 18:36:13 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @shaug on GitHub (Oct 15, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5340

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Set up a better-auth backend with email verification enabled
  2. Create a user account with email verification required
  3. Attempt to verify email with a callbackURL containing query parameters or special characters
  4. Example callbackURL: "https://example.com/dashboard?tab=settings&user=123"
  5. Observe that the generated verification URL is malformed and breaks the authentication flow

Code example:

// Backend setup
import { betterAuth } from "better-auth"

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true
  },
  emailVerification: {
    enabled: true,
    sendVerificationEmail: async ({ user, url }) => {
      console.log("Generated URL:", url); // This will be malformed
    }
  },
});

// Frontend usage
await authClient.signUp.email({
  email: "user@example.com",
  password: "password123",
  callbackURL: "https://example.com/dashboard?tab=settings&user=123" // This breaks the URL
});

Current vs. Expected behavior

Current behavior: When a callbackURL contains special characters like ?, &, #, or spaces, the generated authentication URLs become malformed and break the authentication flow.

Example of broken URL:

/verify-email?token=abc123&callbackURL=https://example.com/dashboard?tab=settings&user=123

The callbackURL's query parameters interfere with the main URL's query parameters, causing the authentication flow to fail.

Expected behavior: The callbackURL should be properly URL-encoded using encodeURIComponent() before being included in the query string.

Example of correct URL:

/verify-email?token=abc123&callbackURL=https%3A//example.com/dashboard%3Ftab%3Dsettings%26user%3D123

This affects multiple authentication flows including email verification, password reset, OAuth2 account linking, and user account management.

What version of Better Auth are you using?

1,3.27

System info

# The script kept exiting without output in my pnpm project, but here's its output elsewhere:

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000",
    "release": "24.6.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.35 GB"
  },
  "node": {
    "version": "v22.16.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null,
    "error": "ENOENT: no such file or directory, open 'package.json'"
  }
}

Which area(s) are affected? (Select all that apply)

Backend, Client, Package

Auth config (if applicable)

import { betterAuth } from "better-auth"

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true
  },
  emailVerification: {
    enabled: true,
    sendVerificationEmail: async ({ user, url }) => {
      // URL will be malformed if callbackURL contains special characters
      console.log("Verification URL:", url);
    }
  },
});

Additional context

Files requiring fixes:

  1. packages/better-auth/src/api/routes/email-verification.ts (lines 54, 367)
  2. packages/better-auth/src/api/routes/sign-up.ts (line 280)
  3. packages/better-auth/src/api/routes/update-user.ts (lines 508, 773, 812)
  4. packages/better-auth/src/api/routes/sign-in.ts (line 523)
  5. packages/better-auth/src/plugins/username/index.ts (line 346)
  6. packages/better-auth/src/oauth2/link-account.ts (line 184)

Correctly implemented examples:

  • packages/better-auth/src/api/routes/reset-password.ts (lines 123, 232)
  • packages/better-auth/src/plugins/oauth-proxy/index.ts (line 212)
  • packages/stripe/src/index.ts (lines 519, 751)

Proposed fix: Apply encodeURIComponent() to all callbackURL parameters:

// Before (broken)
const url = `${baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;

// After (fixed)
const url = `${baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL)}`;

This issue affects core authentication flows and breaks functionality for users with complex callback URLs containing query parameters, fragments, or special characters.

Originally created by @shaug on GitHub (Oct 15, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5340 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Set up a better-auth backend with email verification enabled 2. Create a user account with email verification required 3. Attempt to verify email with a callbackURL containing query parameters or special characters 4. Example callbackURL: `"https://example.com/dashboard?tab=settings&user=123"` 5. Observe that the generated verification URL is malformed and breaks the authentication flow **Code example:** ```typescript // Backend setup import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, emailVerification: { enabled: true, sendVerificationEmail: async ({ user, url }) => { console.log("Generated URL:", url); // This will be malformed } }, }); // Frontend usage await authClient.signUp.email({ email: "user@example.com", password: "password123", callbackURL: "https://example.com/dashboard?tab=settings&user=123" // This breaks the URL }); ``` ### Current vs. Expected behavior **Current behavior:** When a `callbackURL` contains special characters like `?`, `&`, `#`, or spaces, the generated authentication URLs become malformed and break the authentication flow. **Example of broken URL:** ``` /verify-email?token=abc123&callbackURL=https://example.com/dashboard?tab=settings&user=123 ``` The callbackURL's query parameters interfere with the main URL's query parameters, causing the authentication flow to fail. **Expected behavior:** The `callbackURL` should be properly URL-encoded using `encodeURIComponent()` before being included in the query string. **Example of correct URL:** ``` /verify-email?token=abc123&callbackURL=https%3A//example.com/dashboard%3Ftab%3Dsettings%26user%3D123 ``` This affects multiple authentication flows including email verification, password reset, OAuth2 account linking, and user account management. ### What version of Better Auth are you using? 1,3.27 ### System info ```bash # The script kept exiting without output in my pnpm project, but here's its output elsewhere: { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000", "release": "24.6.0", "cpuCount": 10, "cpuModel": "Apple M1 Pro", "totalMemory": "32.00 GB", "freeMemory": "0.35 GB" }, "node": { "version": "v22.16.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null, "error": "ENOENT: no such file or directory, open 'package.json'" } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client, Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, emailVerification: { enabled: true, sendVerificationEmail: async ({ user, url }) => { // URL will be malformed if callbackURL contains special characters console.log("Verification URL:", url); } }, }); ``` ### Additional context **Files requiring fixes:** 1. `packages/better-auth/src/api/routes/email-verification.ts` (lines 54, 367) 2. `packages/better-auth/src/api/routes/sign-up.ts` (line 280) 3. `packages/better-auth/src/api/routes/update-user.ts` (lines 508, 773, 812) 4. `packages/better-auth/src/api/routes/sign-in.ts` (line 523) 5. `packages/better-auth/src/plugins/username/index.ts` (line 346) 6. `packages/better-auth/src/oauth2/link-account.ts` (line 184) **Correctly implemented examples:** - `packages/better-auth/src/api/routes/reset-password.ts` (lines 123, 232) - `packages/better-auth/src/plugins/oauth-proxy/index.ts` (line 212) - `packages/stripe/src/index.ts` (lines 519, 751) **Proposed fix:** Apply `encodeURIComponent()` to all `callbackURL` parameters: ```typescript // Before (broken) const url = `${baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; // After (fixed) const url = `${baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL)}`; ``` This issue affects core authentication flows and breaks functionality for users with complex callback URLs containing query parameters, fragments, or special characters.
GiteaMirror added the lockedbug labels 2026-04-17 18:36:14 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 15, 2025):

You are correct—improper encoding of the callbackURL parameter is a real and known issue in the Better Auth codebase. In the files and lines you listed (such as email-verification.ts, sign-up.ts, update-user.ts, sign-in.ts, username plugin, and OAuth2 linking), callbackURL is included in URLs without encodeURIComponent(), which leads to malformed URLs when the callback contains query parameters or special characters. This breaks authentication flows as you described. The correctly implemented examples you cited (like reset-password.ts and oauth-proxy) do use encodeURIComponent() and do not have this bug.

There are also related GitHub issues confirming this problem and discussing workarounds, such as double-encoding the callbackURL or manually parsing parameters on the frontend for affected versions. The robust fix is to apply encodeURIComponent() to all callbackURL parameters in backend URL construction, as you proposed. One additional location to check is the magic-link plugin, which uses URLSearchParams (and thus handles encoding correctly), but most other core flows need explicit fixes. For reference, a fix was implemented in PR #3863, but it may not be present in your version (1.3.27) yet—upgrading to a version with this fix is recommended if possible.

You can see more details and discussion in these issues:
Issue 5015
Issue 3812
Issue 5030

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3408480290 --> @dosubot[bot] commented on GitHub (Oct 15, 2025): <!-- Answer --> You are correct—improper encoding of the callbackURL parameter is a real and known issue in the Better Auth codebase. In the files and lines you listed (such as email-verification.ts, sign-up.ts, update-user.ts, sign-in.ts, username plugin, and OAuth2 linking), callbackURL is included in URLs without encodeURIComponent(), which leads to malformed URLs when the callback contains query parameters or special characters. This breaks authentication flows as you described. The correctly implemented examples you cited (like reset-password.ts and oauth-proxy) do use encodeURIComponent() and do not have this bug. There are also related GitHub issues confirming this problem and discussing workarounds, such as double-encoding the callbackURL or manually parsing parameters on the frontend for affected versions. The robust fix is to apply encodeURIComponent() to all callbackURL parameters in backend URL construction, as you proposed. One additional location to check is the magic-link plugin, which uses URLSearchParams (and thus handles encoding correctly), but most other core flows need explicit fixes. For reference, a fix was implemented in PR #3863, but it may not be present in your version (1.3.27) yet—upgrading to a version with this fix is recommended if possible. You can see more details and discussion in these issues: [Issue 5015](https://github.com/better-auth/better-auth/issues/5015) [Issue 3812](https://github.com/better-auth/better-auth/issues/3812) [Issue 5030](https://github.com/better-auth/better-auth/issues/5030) <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/06e6ede4-a689-489b-874b-8782e63d228a?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5340)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27540