mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
/**
|
|
* The `?next=` round-trip for the admin area.
|
|
*
|
|
* A connector link to an admin panel lands on `/admin/...` with no admin
|
|
* session, the layout bounces to `/admin/login`, and without this the admin
|
|
* would log in and arrive at the dashboard instead of the extension they
|
|
* clicked Install on.
|
|
*
|
|
* This is the one place in the whole connector flow where a *path* comes out
|
|
* of a URL rather than out of a fixed template, so it gets the strictest
|
|
* check in it. Everything that is not unmistakably an admin path inside this
|
|
* app is thrown away and the login falls back to `/admin`.
|
|
*/
|
|
|
|
const SAFE = /^\/admin(?:\/[A-Za-z0-9._~-]+)*\/?$/;
|
|
|
|
/**
|
|
* Returns a path safe to `router.replace()` after login, or null.
|
|
*
|
|
* Refuses anything that could leave this origin or this area: a scheme, a
|
|
* protocol-relative `//host`, a backslash (which some browsers normalise to
|
|
* `/`), an encoded separator, a `..` segment, or a path outside `/admin`.
|
|
* Two queries are allowed. `?tab=` is validated by the caller against the
|
|
* real tab list - the admin shell keeps its tab in a store, so that is how a
|
|
* link reaches a specific panel. `?version=` is the version a connector
|
|
* install link asked for (`/admin/marketplace/<slug>?version=1.2.0`); the
|
|
* marketplace page checks it again against the directory before offering it.
|
|
*/
|
|
export function safeAdminNext(raw: string | null | undefined): string | null {
|
|
if (!raw) return null;
|
|
|
|
let value = raw;
|
|
// One decode, so `%2f%2fevil` cannot smuggle a separator past the checks
|
|
// below. A value that is not valid percent-encoding is not a path we wrote.
|
|
try {
|
|
value = decodeURIComponent(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
if (value.length > 512) return null;
|
|
if (value.includes('\\') || value.includes('\0')) return null;
|
|
if (value.startsWith('//')) return null;
|
|
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return null;
|
|
|
|
const [path, query = ''] = value.split('?', 2);
|
|
if (!SAFE.test(path)) return null;
|
|
if (path.split('/').includes('..')) return null;
|
|
// Only the tab hand-off or the version pick, each as one plain value.
|
|
if (query && !/^tab=[a-z_]{1,32}$/.test(query) && !/^version=[0-9A-Za-z.-]{1,64}$/.test(query)) {
|
|
return null;
|
|
}
|
|
|
|
return query ? `${path}?${query}` : path;
|
|
}
|