-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathredirect.js
More file actions
116 lines (86 loc) · 2.91 KB
/
redirect.js
File metadata and controls
116 lines (86 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
(function () {
"use strict";
/* Allowed hosts */
const allowedLocalHosts = [
"localhost",
"127.0.0.1",
];
/* Private domains (optional future use) */
const allowedDomains = [
];
/* Freeze configuration */
Object.freeze(allowedLocalHosts);
Object.freeze(allowedDomains);
/* Port range */
const MIN_PORT = 1;
const MAX_PORT = 65535;
function fail(msg) {
document.body.textContent = msg;
throw new Error(msg);
};
/* Read query parameters */
const params = new URLSearchParams(window.location.search);
const state = params.get("state");
if (!state) fail("Missing state parameter");
/* Decode state safely & Trim whitespace to prevent bypass tricks */
const cleanState = (() => {
const trimmed = state.trim();
try {
return decodeURIComponent(trimmed);
} catch {
return trimmed;
}
})();
/* Optional safety */
if (cleanState.length > 2000) fail("State too long");
/*
Expected state format:
origin|/path|anything csrf
Examples:
http://localhost:3032|/oauth/callback|userId=49fj34
https://dev.yourcompany.com|/oauth/callback|D45DT3SQW4DH46VC|eru@#djD325
*/
const { origin, path } = (() => {
const [rawOrigin, rawPath] = cleanState.split("|", 2);
if (!rawOrigin) fail("Missing origin");
return {
origin: rawOrigin,
path: rawPath || "/"
};
})();
/* Validate origin */
const parsedOrigin = (() => {
try {
return new URL(origin);
} catch {
fail("Invalid origin");
}
})();
/* Restrict protocol */
if (!["http:", "https:"].includes(parsedOrigin.protocol)) fail("Invalid protocol");
/* Prevent credentials */
if (parsedOrigin.username || parsedOrigin.password) fail("Credentials not allowed");
/* Validate localhost */
if (allowedLocalHosts.includes(parsedOrigin.hostname)) {
if (!parsedOrigin.port) fail("Localhost must include port");
const port = Number(parsedOrigin.port);
/* Validate port */
if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) fail("Invalid port");
}
/* Validate private domains */
else if (!allowedDomains.includes(parsedOrigin.hostname)) {
fail("Host not allowed");
}
/* Validate path */
if (!path.startsWith("/")) fail("Invalid path");
if (path.includes("//")) fail("Invalid path");
if (path.includes("?")) fail("Path cannot contain query");
if (path.includes("#")) fail("Path cannot contain fragment");
/* Build redirect URL safely */
const url = new URL(path, parsedOrigin.origin);
/* Preserve OAuth query parameters */
url.search = window.location.search;
const target = url.toString();
/* Redirect */
window.location.replace(target);
})();