better-auth
Version:
The most comprehensive authentication framework for TypeScript.
105 lines (104 loc) • 4.19 kB
JavaScript
import { wildcardMatch } from "../utils/wildcard.mjs";
import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
//#region src/auth/trusted-origins.ts
/**
* Resolves `.` and `..` segments in a path after percent-decoding so a
* path-pinned pattern cannot be bypassed with traversal: e.g.
* `myapp://host/cb/../evil` must not satisfy pattern `myapp://host/cb`.
* Returns "" for an empty or root path.
*/
const normalizePath = (path) => {
let decoded = path;
try {
decoded = decodeURIComponent(path);
} catch {}
const segments = [];
for (const segment of decoded.split("/")) if (segment === "..") segments.pop();
else if (segment !== "." && segment !== "") segments.push(segment);
return segments.length > 0 ? `/${segments.join("/")}` : "";
};
/**
* Splits a custom-scheme origin into its scheme, authority and path using
* plain string operations.
*
* `new URL()` is deliberately avoided here: its parsing of non-special schemes
* (e.g. `myapp://`, `exp://`) is not consistent across the runtimes Better Auth
* targets (Node, Bun, Deno, Cloudflare Workers), and the result of an origin
* check must not depend on which engine extracts the authority.
*
* Scheme and authority are lower-cased (matching how a URL canonicalizes its
* host); the path is percent-decoded and resolved so traversal cannot bypass
* a path-pinned pattern.
*/
const parseCustomSchemeOrigin = (value) => {
const schemeEnd = value.indexOf(":");
if (schemeEnd <= 0) return null;
const scheme = value.slice(0, schemeEnd).toLowerCase();
let rest = value.slice(schemeEnd + 1);
let authority = "";
if (rest.startsWith("//")) {
rest = rest.slice(2);
const authorityEnd = rest.search(/[/?#]/);
if (authorityEnd === -1) {
authority = rest;
rest = "";
} else {
authority = rest.slice(0, authorityEnd);
rest = rest.slice(authorityEnd);
}
}
const path = normalizePath(rest.replace(/[?#].*$/, ""));
return {
scheme,
authority: authority.toLowerCase(),
path
};
};
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
/**
* Validates root-relative redirects against ambiguous browser and router parsing.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-4.2
* @see https://url.spec.whatwg.org/#concept-basic-url-parser
*/
const isSafeRelativeURL = (value) => {
if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\") || CONTROL_CHARACTER_PATTERN.test(value)) return false;
const pathEnd = value.search(/[?#]/);
const path = pathEnd === -1 ? value : value.slice(0, pathEnd);
if (ENCODED_PATH_SEPARATOR_PATTERN.test(path)) return false;
try {
return new URL(value, RELATIVE_URL_PARSER_ORIGIN).origin === RELATIVE_URL_PARSER_ORIGIN;
} catch {
return false;
}
};
/**
* Matches the given url against an origin or origin pattern
* See "options.trustedOrigins" for details of supported patterns
*
* @param url The url to test
* @param pattern The origin pattern
* @param [settings] Specify supported pattern matching settings
* @returns {boolean} true if the URL matches the origin pattern, false otherwise.
*/
const matchesOriginPattern = (url, pattern, settings) => {
if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
if (pattern.includes("*") || pattern.includes("?")) {
if (pattern.includes("://")) return wildcardMatch(pattern)(getOrigin(url) || url);
const host = getHost(url);
if (!host) return false;
return wildcardMatch(pattern)(host);
}
const protocol = getProtocol(url);
if (protocol === "http:" || protocol === "https:" || !protocol) return pattern === getOrigin(url);
const parsed = parseCustomSchemeOrigin(url);
const parsedPattern = parseCustomSchemeOrigin(pattern);
if (!parsed || !parsedPattern || parsed.scheme !== parsedPattern.scheme) return false;
if (parsedPattern.authority && parsed.authority !== parsedPattern.authority) return false;
if (!parsedPattern.path) return true;
return parsed.path === parsedPattern.path || parsed.path.startsWith(`${parsedPattern.path}/`);
};
//#endregion
export { matchesOriginPattern };