Files
esign/packages/lib/utils/is-valid-return-to.ts
zooqueenandHanzo Dev 6ac5643e31 Return a sign-in only to a path on this app
normalizeReturnTo accepts a path, or an absolute URL on the app's origin as
its path, and refuses anything a browser could read as another host: a
path that begins // or /\ once dot segments and encodings are resolved,
and any backslash or control character. isValidReturnTo is the same
question.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 02:52:18 -07:00

58 lines
1.5 KiB
TypeScript

import { NEXT_PUBLIC_WEBAPP_URL } from '@hanzo/esign-lib/constants/app';
/**
* The path on this app a `returnTo` names, or undefined when it names anywhere
* else.
*
* Accepted: a path (`/documents?x=1`) or an absolute URL on the app's own
* origin, returned as its path. Refused: anything that resolves to another
* host, and anything a browser could read as one — a path beginning `//` or
* `/\` once dot segments, encodings and stray characters are resolved
* (`/.//evil.com`, `/%2F%2Fevil.com`, `/\evil.com`), backslashes and control
* characters anywhere.
*/
export const normalizeReturnTo = (returnTo?: string): string | undefined => {
if (!returnTo) {
return undefined;
}
let decoded: string;
try {
decoded = decodeURIComponent(returnTo);
} catch {
return undefined;
}
// eslint-disable-next-line no-control-regex
if (/[\\\u0000-\u001f\u007f]/.test(decoded) || decoded.startsWith('//')) {
return undefined;
}
const origin = new URL(NEXT_PUBLIC_WEBAPP_URL()).origin;
let url: URL;
try {
url = new URL(decoded, origin);
} catch {
return undefined;
}
if (url.origin !== origin) {
return undefined;
}
const path = `${url.pathname}${url.search}${url.hash}`;
if (!path.startsWith('/') || path.startsWith('//') || path.startsWith('/\\')) {
return undefined;
}
return path;
};
/** Whether `returnTo` names a path on this app (see normalizeReturnTo). */
export const isValidReturnTo = (returnTo?: string): boolean =>
normalizeReturnTo(returnTo) !== undefined;