mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +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", () => {
|
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);
|
redirectToDuoFrameless(validUrl);
|
||||||
expect(window.location.href).toBe(validUrl);
|
expect(window.location.href).toBe(validUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should redirect to a valid Duo Federal URL", () => {
|
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);
|
redirectToDuoFrameless(validUrl);
|
||||||
expect(window.location.href).toBe(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", () => {
|
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");
|
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", () => {
|
it("should throw an error for a non-HTTPS URL", () => {
|
||||||
const nonHttpsUrl = "http://api-123.duosecurity.com/auth";
|
const nonHttpsUrl = "http://api-123.duosecurity.com/auth";
|
||||||
expect(() => redirectToDuoFrameless(nonHttpsUrl)).toThrow("Invalid redirect URL");
|
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", () => {
|
it("should throw an error for a URL with an invalid hostname", () => {
|
||||||
const invalidHostnameUrl = "https://api-123.invalid.com";
|
const invalidHostnameUrl = "https://api-123.invalid.com";
|
||||||
expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL");
|
expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL");
|
||||||
|
|||||||
@@ -57,29 +57,46 @@ window.addEventListener("load", async () => {
|
|||||||
* @param redirectUrl the duo auth url
|
* @param redirectUrl the duo auth url
|
||||||
*/
|
*/
|
||||||
export function redirectToDuoFrameless(redirectUrl: string) {
|
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:
|
* This regex checks for the following:
|
||||||
* The string must start with "https://api-"
|
* The hostname must start with a subdomain that begins with "api-" followed by a
|
||||||
* Followed by a subdomain that can contain letters, numbers
|
* string that can contain letters or numbers of indeterminate length
|
||||||
* Followed by either "duosecurity.com" or "duofederal.com"
|
* Followed by either "duosecurity.com" or "duofederal.com"
|
||||||
* This ensures that the redirect does not contain any malicious content
|
* This ensures that the redirect does not contain any malicious content
|
||||||
* and is a valid Duo URL.
|
* and is a valid Duo URL.
|
||||||
* */
|
* */
|
||||||
const duoRedirectUrlRegex = /^https:\/\/api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com/;
|
const duoRedirectUrlRegex = /^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 validateUrl = new URL(redirectUrl);
|
const validateUrl = new URL(redirectUrl);
|
||||||
// URLs should not contain
|
|
||||||
// Check that no embedded credentials are present
|
// Check that no embedded credentials are present
|
||||||
if (validateUrl.username || validateUrl.password) {
|
if (validateUrl.username || validateUrl.password) {
|
||||||
throw new Error("Invalid redirect URL: embedded credentials not allowed");
|
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