Skip to content

Commit 04b741a

Browse files
authored
Merge pull request #161 from zextras/version-bumper/v0.9.36-1
Version bumper/v0.9.36 1
2 parents 8ecccc0 + 0f2018e commit 04b741a

6 files changed

Lines changed: 116 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [0.9.36](https://github.com/zextras/carbonio-login-ui/compare/v0.9.35...v0.9.36) (2026-03-31)
6+
7+
8+
### Bug Fixes
9+
10+
* **CO-3425:** use publicUrl instead of adminConsolePublicUrl for login redirect [#160](https://github.com/zextras/carbonio-login-ui/issues/160) ([cf0ddf2](https://github.com/zextras/carbonio-login-ui/commit/cf0ddf2f9ccadd30146e0b020ca6864206f09b7d))
11+
* use publicUrl instead of adminConsolePublicUrl for login redirect ([8ee1d0f](https://github.com/zextras/carbonio-login-ui/commit/8ee1d0f02100af126febe6c075fd0d2d66967a29))
12+
13+
### [0.9.35](https://github.com/zextras/carbonio-login-ui/compare/v0.9.34...v0.9.35) (2026-03-27)
14+
15+
16+
### Bug Fixes
17+
18+
* ensure safe redirect URLs in login flow ([b3ce671](https://github.com/zextras/carbonio-login-ui/commit/b3ce671c1a29af845a8867f811aaf0f39c0fa3f3))
19+
* improve isSafeRedirect function logic ([2bc87f6](https://github.com/zextras/carbonio-login-ui/commit/2bc87f63338a3635027d839bb6bb3091460808f5))
20+
521
### [0.9.34](https://github.com/zextras/carbonio-login-ui/compare/v0.9.33...v0.9.34) (2026-02-27)
622

723

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "carbonio-login-ui",
3-
"version": "0.9.34",
3+
"version": "0.9.36",
44
"description": "Zextras authentication App",
55
"main": "src/index.html",
66
"scripts": {

src/components-v1/page-layout.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { useGetPrimaryColor } from '../primary-color/use-get-primary-color';
4848
import { getLoginConfig } from '../services/login-page-services';
4949
import { useLoginConfigStore } from '../store/login/store';
5050
import { ThemeCallbacksContext } from '../theme-provider/theme-provider';
51-
import { prepareUrlForForward } from '../utils';
51+
import { isSafeRedirect, prepareUrlForForward } from '../utils';
5252

5353
type LoginContainerProps = {
5454
backgroundImage: string;
@@ -118,6 +118,11 @@ function DarkReaderListener(): React.JSX.Element | null {
118118
return null;
119119
}
120120

121+
const getSafeRedirectUrl = (url: string | null): string | null => {
122+
if (url === null) return null;
123+
return isSafeRedirect(url) ? prepareUrlForForward(url) : '/';
124+
};
125+
121126
export default function PageLayout({
122127
version,
123128
isAdvanced
@@ -136,9 +141,8 @@ export default function PageLayout({
136141
const [serverError, setServerError] = useState(false);
137142

138143
const urlParams = new URLSearchParams(window.location.search);
139-
const [destinationUrl, setDestinationUrl] = useState(
140-
prepareUrlForForward(urlParams.get('destinationUrl'))
141-
);
144+
const safeRedirectUrl = getSafeRedirectUrl(urlParams.get('destinationUrl'));
145+
const [destinationUrl, setDestinationUrl] = useState(safeRedirectUrl);
142146
const [domain, setDomain] = useState(urlParams.get('domain') ?? destinationUrl);
143147

144148
const [bg, setBg] = useState(backgroundImage);
@@ -168,7 +172,11 @@ export default function PageLayout({
168172
if (isAdvanced) {
169173
getLoginConfig(version, domain, domain)
170174
.then((res) => {
171-
if (!destinationUrl) setDestinationUrl(prepareUrlForForward(res.publicUrl));
175+
if (!destinationUrl) {
176+
const targetUrl = prepareUrlForForward(res.publicUrl);
177+
const safeDestinationUrl = isSafeRedirect(targetUrl) ? targetUrl : '/';
178+
setDestinationUrl(safeDestinationUrl);
179+
}
172180
if (!domain) setDomain(res.zimbraDomainName);
173181
setDomainName(res.zimbraDomainName);
174182

src/tests/utils.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Zextras <https://www.zextras.com>
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
*/
6+
/* eslint-disable no-script-url */
7+
8+
import { isSafeRedirect } from '../utils';
9+
10+
const ORIGIN = 'https://mail.example.com';
11+
12+
describe('isSafeRedirect', () => {
13+
const originalLocation = window.location;
14+
15+
beforeEach(() => {
16+
Object.defineProperty(window, 'location', {
17+
writable: true,
18+
value: { ...originalLocation, origin: ORIGIN }
19+
});
20+
});
21+
22+
afterEach(() => {
23+
Object.defineProperty(window, 'location', {
24+
writable: true,
25+
value: originalLocation
26+
});
27+
});
28+
29+
describe('should allow safe URLs', () => {
30+
it.each([
31+
['/', 'root path'],
32+
['/inbox', 'simple relative path'],
33+
['/mail/inbox?page=1', 'relative path with query string'],
34+
['/settings#account', 'relative path with fragment'],
35+
[`${ORIGIN}/inbox`, 'absolute same-origin URL'],
36+
[`${ORIGIN}/mail/inbox?page=1&sort=date`, 'absolute same-origin with query params'],
37+
['relative-path', 'plain relative segment'],
38+
['', 'empty string'],
39+
['https://saml-validation.com', 'https external domain'],
40+
['http://saml-validation.com', 'http external domain'],
41+
['http://mail.example.com/inbox', 'same host but different scheme (http vs https)']
42+
])('%s (%s)', (url) => {
43+
expect(isSafeRedirect(url)).toBe(true);
44+
});
45+
});
46+
47+
describe('should block dangerous URLs', () => {
48+
it.each([
49+
['javascript:alert(document.cookie)', 'javascript: scheme'],
50+
['javascript:void(0)', 'javascript:void'],
51+
['javascript:eval(alert(1))', 'javascript:eval'],
52+
['data:text/html,<script>alert(1)</script>', 'data: URI with script'],
53+
['data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==', 'data: URI base64 encoded'],
54+
['vbscript:msgbox("xss")', 'vbscript: scheme'],
55+
['blob:https://evil.com/some-id', 'blob: URI'],
56+
['ftp://files.example.com/secret', 'ftp: scheme']
57+
])('%s (%s)', (url) => {
58+
expect(isSafeRedirect(url)).toBe(false);
59+
});
60+
});
61+
62+
describe('edge cases', () => {
63+
it('should block falsy values', () => {
64+
expect(isSafeRedirect(null)).toBe(false);
65+
});
66+
67+
it('should handle backslash-based bypass attempts', () => {
68+
const result = isSafeRedirect('\\\\evil.com');
69+
expect(result).toBe(false);
70+
});
71+
});
72+
});

src/utils.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,15 @@ export const setCookie = (cName, cValue, expDays) => {
120120
expDays && Number.isInteger(expDays) ? `expires=${date.toUTCString()}` : undefined;
121121
document.cookie = `${cName}=${cValue}; ${expires || ''}; path=/`;
122122
};
123+
124+
export const isSafeRedirect = (url) => {
125+
if (typeof url !== 'string') return false;
126+
try {
127+
// eslint-disable-next-line no-undef
128+
const parsed = new URL(url, globalThis.location.origin);
129+
if (/[\\]/.test(url)) return false;
130+
return ['http:', 'https:'].includes(parsed.protocol);
131+
} catch {
132+
return false;
133+
}
134+
};

0 commit comments

Comments
 (0)