mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 18:43:26 +00:00
Merge branch 'main' into km/cose-upgrade
This commit is contained in:
@@ -148,7 +148,7 @@ export class StripeService {
|
||||
base: {
|
||||
color: null,
|
||||
fontFamily:
|
||||
'"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
|
||||
'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
|
||||
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||
fontSize: "16px",
|
||||
fontSmoothing: "antialiased",
|
||||
|
||||
@@ -112,13 +112,15 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
|
||||
|
||||
this.activeSponsorshipOrgs$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)));
|
||||
|
||||
.pipe(
|
||||
map((orgs) =>
|
||||
orgs.filter((o) => o.familySponsorshipFriendlyName !== null && !o.isAdminInitiated),
|
||||
),
|
||||
);
|
||||
this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
|
||||
|
||||
this.loading = false;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon
|
||||
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
|
||||
import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component";
|
||||
import { ProfileComponent } from "../auth/settings/account/profile.component";
|
||||
import { SelectableAvatarComponent } from "../auth/settings/account/selectable-avatar.component";
|
||||
import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component";
|
||||
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
|
||||
import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component";
|
||||
@@ -39,7 +40,6 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp
|
||||
import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component";
|
||||
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
|
||||
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
|
||||
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
|
||||
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../dirt/reports/pages/organizations/exposed-passwords-report.component";
|
||||
import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../dirt/reports/pages/organizations/inactive-two-factor-report.component";
|
||||
import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../dirt/reports/pages/organizations/reused-passwords-report.component";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
|
||||
<text fill="%23FBFBFB" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
|
||||
<text fill="%23FBFBFB" x="50%" y="50%" font-family="\'Roboto\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
|
||||
font-size="18" text-anchor="middle">
|
||||
Loading...
|
||||
</text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
|
||||
<text fill="%23333333" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
|
||||
<text fill="%23333333" x="50%" y="50%" font-family="\'Roboto\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
|
||||
font-size="18" text-anchor="middle">
|
||||
Loading...
|
||||
</text>
|
||||
|
||||
@@ -21,7 +21,7 @@ $body-bg: $white;
|
||||
$body-color: #333333;
|
||||
|
||||
$font-family-sans-serif:
|
||||
"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
|
||||
$h1-font-size: 1.7rem;
|
||||
|
||||
Reference in New Issue
Block a user