mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-17 17:01:18 -05:00
fix(core): make redirect-uri validation runtime-safe and reject fragments (#9845)
This commit is contained in:
7
.changeset/safe-url-runtime-fragment.md
Normal file
7
.changeset/safe-url-runtime-fragment.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@better-auth/oauth-provider": patch
|
||||
"@better-auth/core": patch
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Harden redirect-URI validation across the OAuth provider plugins. `isSafeUrlScheme` and `SafeUrlSchema` no longer call `URL.canParse`, which is absent on some supported runtimes and could throw or silently disable the dangerous-scheme check. They now parse with a `try`/`catch` fallback. `SafeUrlSchema` also rejects redirect URIs that contain a fragment component, per RFC 6749 §3.1.2.
|
||||
@@ -7,6 +7,7 @@ import { DANGEROUS_URL_SCHEMES } from "./url";
|
||||
* server stores and later hands back to a browser.
|
||||
*
|
||||
* - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
|
||||
* - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
|
||||
* - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
|
||||
* `*.localhost` per RFC 6761), where HTTP is allowed for local development.
|
||||
* - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
|
||||
@@ -16,7 +17,10 @@ import { DANGEROUS_URL_SCHEMES } from "./url";
|
||||
* rather than re-implementing the scheme policy per plugin.
|
||||
*/
|
||||
export const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
||||
if (!URL.canParse(val)) {
|
||||
let u: URL;
|
||||
try {
|
||||
u = new URL(val);
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "URL must be parseable",
|
||||
@@ -25,8 +29,6 @@ export const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
const u = new URL(val);
|
||||
|
||||
if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
@@ -35,6 +37,13 @@ export const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (val.includes("#")) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Redirect URI must not contain a fragment component",
|
||||
});
|
||||
}
|
||||
|
||||
if (u.protocol === "http:" && !isLoopbackHost(u.host)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
|
||||
@@ -46,4 +46,16 @@ describe("SafeUrlSchema", () => {
|
||||
);
|
||||
expect(SafeUrlSchema.safeParse("http://127.0.0.1/cb").success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects redirect URIs with a fragment component", () => {
|
||||
expect(
|
||||
SafeUrlSchema.safeParse("https://example.com/cb#token").success,
|
||||
).toBe(false);
|
||||
expect(SafeUrlSchema.safeParse("https://example.com/cb#").success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(SafeUrlSchema.safeParse("https://example.com/cb").success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,9 +60,12 @@ export const DANGEROUS_URL_SCHEMES = ["javascript:", "data:", "vbscript:"];
|
||||
* execution schemes without rejecting relative paths or mobile deep links.
|
||||
*/
|
||||
export function isSafeUrlScheme(value: string): boolean {
|
||||
if (!URL.canParse(value)) {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(value);
|
||||
} catch {
|
||||
// Relative URLs carry no scheme to abuse.
|
||||
return true;
|
||||
}
|
||||
return !DANGEROUS_URL_SCHEMES.includes(new URL(value).protocol);
|
||||
return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user