1
0
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:
Matt Andreko
2025-05-12 07:56:50 -04:00
committed by GitHub
parent 5408a62b7d
commit fcaf5e63c5
2 changed files with 71 additions and 14 deletions

View File

@@ -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");

View File

@@ -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;
}
/**