mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
Duo URL redirect enhancements (#14640)
* Provide additional scrutiny on Duo redirect filtering * Address review feedback from Jared * Add documentation to redirectToDuoFrameless method
This commit is contained in:
@@ -10,13 +10,13 @@ describe("duo-redirect", () => {
|
||||
});
|
||||
|
||||
it("should redirect to a valid Duo URL", () => {
|
||||
const validUrl = "https://api-123.duosecurity.com/auth";
|
||||
const validUrl = "https://api-123.duosecurity.com/oauth/v1/authorize";
|
||||
redirectToDuoFrameless(validUrl);
|
||||
expect(window.location.href).toBe(validUrl);
|
||||
});
|
||||
|
||||
it("should redirect to a valid Duo Federal URL", () => {
|
||||
const validUrl = "https://api-123.duofederal.com/auth";
|
||||
const validUrl = "https://api-123.duofederal.com/oauth/v1/authorize";
|
||||
redirectToDuoFrameless(validUrl);
|
||||
expect(window.location.href).toBe(validUrl);
|
||||
});
|
||||
@@ -27,15 +27,55 @@ describe("duo-redirect", () => {
|
||||
});
|
||||
|
||||
it("should throw an error for an malicious URL with valid redirect embedded", () => {
|
||||
const invalidUrl = "https://malicious-site.com\\@api-123.duosecurity.com/auth";
|
||||
const invalidUrl = "https://malicious-site.com\\@api-123.duosecurity.com/oauth/v1/authorize";
|
||||
expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL");
|
||||
});
|
||||
|
||||
it("should throw an error for a URL with a malicious subdomain", () => {
|
||||
const maliciousSubdomainUrl =
|
||||
"https://api-a86d5bde.duosecurity.com.evil.com/oauth/v1/authorize";
|
||||
expect(() => redirectToDuoFrameless(maliciousSubdomainUrl)).toThrow("Invalid redirect URL");
|
||||
});
|
||||
|
||||
it("should throw an error for a URL using HTTP protocol", () => {
|
||||
const maliciousSubdomainUrl = "http://api-a86d5bde.duosecurity.com/oauth/v1/authorize";
|
||||
expect(() => redirectToDuoFrameless(maliciousSubdomainUrl)).toThrow(
|
||||
"Invalid redirect URL: invalid protocol",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error for a URL with javascript code", () => {
|
||||
const maliciousSubdomainUrl = "javascript://https://api-a86d5bde.duosecurity.com%0Aalert(1)";
|
||||
expect(() => redirectToDuoFrameless(maliciousSubdomainUrl)).toThrow(
|
||||
"Invalid redirect URL: invalid protocol",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error for a non-HTTPS URL", () => {
|
||||
const nonHttpsUrl = "http://api-123.duosecurity.com/auth";
|
||||
expect(() => redirectToDuoFrameless(nonHttpsUrl)).toThrow("Invalid redirect URL");
|
||||
});
|
||||
|
||||
it("should throw an error for a URL with invalid port specified", () => {
|
||||
const urlWithPort = "https://api-123.duyosecurity.com:8080/auth";
|
||||
expect(() => redirectToDuoFrameless(urlWithPort)).toThrow(
|
||||
"Invalid redirect URL: port not allowed",
|
||||
);
|
||||
});
|
||||
|
||||
it("should redirect to a valid Duo Federal URL with valid port", () => {
|
||||
const validUrl = "https://api-123.duofederal.com:443/oauth/v1/authorize";
|
||||
redirectToDuoFrameless(validUrl);
|
||||
expect(window.location.href).toBe(validUrl);
|
||||
});
|
||||
|
||||
it("should throw an error for a URL with an invalid pathname", () => {
|
||||
const urlWithPort = "https://api-123.duyosecurity.com/../evil/path/here/";
|
||||
expect(() => redirectToDuoFrameless(urlWithPort)).toThrow(
|
||||
"Invalid redirect URL: invalid pathname",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error for a URL with an invalid hostname", () => {
|
||||
const invalidHostnameUrl = "https://api-123.invalid.com";
|
||||
expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL");
|
||||
|
||||
@@ -57,29 +57,46 @@ window.addEventListener("load", async () => {
|
||||
* @param redirectUrl the duo auth url
|
||||
*/
|
||||
export function redirectToDuoFrameless(redirectUrl: string) {
|
||||
// Regex to match a valid duo redirect URL
|
||||
// Validation for Duo redirect URL to prevent open redirect or XSS vulnerabilities
|
||||
// Only used for Duo 2FA redirects in the extension
|
||||
/**
|
||||
* This regex checks for the following:
|
||||
* The string must start with "https://api-"
|
||||
* Followed by a subdomain that can contain letters, numbers
|
||||
* The hostname must start with a subdomain that begins with "api-" followed by a
|
||||
* string that can contain letters or numbers of indeterminate length
|
||||
* Followed by either "duosecurity.com" or "duofederal.com"
|
||||
* This ensures that the redirect does not contain any malicious content
|
||||
* and is a valid Duo URL.
|
||||
* */
|
||||
const duoRedirectUrlRegex = /^https:\/\/api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com/;
|
||||
// Check if the redirect URL matches the regex
|
||||
if (!duoRedirectUrlRegex.test(redirectUrl)) {
|
||||
throw new Error("Invalid redirect URL");
|
||||
}
|
||||
// At this point we know the URL to be valid, but we need to check for embedded credentials
|
||||
const duoRedirectUrlRegex = /^api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com$/;
|
||||
const validateUrl = new URL(redirectUrl);
|
||||
// URLs should not contain
|
||||
|
||||
// Check that no embedded credentials are present
|
||||
if (validateUrl.username || validateUrl.password) {
|
||||
throw new Error("Invalid redirect URL: embedded credentials not allowed");
|
||||
}
|
||||
|
||||
window.location.href = decodeURIComponent(redirectUrl);
|
||||
// Check that the protocol is HTTPS
|
||||
if (validateUrl.protocol !== "https:") {
|
||||
throw new Error("Invalid redirect URL: invalid protocol");
|
||||
}
|
||||
|
||||
// Check that the port is not specified
|
||||
if (validateUrl.port && validateUrl.port !== "443") {
|
||||
throw new Error("Invalid redirect URL: port not allowed");
|
||||
}
|
||||
|
||||
if (validateUrl.pathname !== "/oauth/v1/authorize") {
|
||||
throw new Error("Invalid redirect URL: invalid pathname");
|
||||
}
|
||||
|
||||
// Check if the redirect hostname matches the regex
|
||||
// Only check the hostname part of the URL to avoid over-zealous Regex expressions from matching
|
||||
// and causing an Open Redirect vulnerability. Always use hostname instead of host, because host includes port if specified.
|
||||
if (!duoRedirectUrlRegex.test(validateUrl.hostname)) {
|
||||
throw new Error("Invalid redirect URL");
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user