Skip to content

Commit 2178cfe

Browse files
authored
fix(editor): Stop nefarious URL redirection in editor middleware (#16047)
1 parent 3f48106 commit 2178cfe

File tree

2 files changed

+80
-23
lines changed

2 files changed

+80
-23
lines changed

packages/frontend/editor-ui/src/utils/rbac/middleware/guest.test.ts

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,80 @@ vi.mock('@/stores/users.store', () => ({
88
}));
99

1010
describe('Middleware', () => {
11+
const ORIGIN_URL = 'https://n8n.local';
12+
13+
beforeEach(() => {
14+
global.window = Object.create(window);
15+
16+
Object.defineProperty(window, 'location', {
17+
value: {
18+
href: '',
19+
origin: ORIGIN_URL,
20+
},
21+
writable: true,
22+
});
23+
});
24+
1125
describe('guest', () => {
12-
it('should redirect to given path if current user is present and valid redirect is provided', async () => {
13-
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType<
14-
typeof useUsersStore
15-
>);
26+
describe('if current user is present', () => {
27+
beforeEach(() => {
28+
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType<
29+
typeof useUsersStore
30+
>);
31+
});
1632

17-
const nextMock = vi.fn();
18-
const toMock = { query: { redirect: '/some-path' } } as unknown as RouteLocationNormalized;
19-
const fromMock = {} as RouteLocationNormalized;
33+
afterEach(() => {
34+
vi.clearAllMocks();
35+
});
2036

21-
await guestMiddleware(toMock, fromMock, nextMock, {});
37+
it('should redirect to given path if valid redirect is provided', async () => {
38+
const nextMock = vi.fn();
39+
const toMock = { query: { redirect: '/some-path' } } as unknown as RouteLocationNormalized;
40+
const fromMock = {} as RouteLocationNormalized;
2241

23-
expect(nextMock).toHaveBeenCalledWith('/some-path');
24-
});
42+
await guestMiddleware(toMock, fromMock, nextMock, {});
2543

26-
it('should redirect to homepage if current user is present and no valid redirect', async () => {
27-
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType<
28-
typeof useUsersStore
29-
>);
44+
expect(nextMock).toHaveBeenCalledWith('/some-path');
45+
});
3046

31-
const nextMock = vi.fn();
32-
const toMock = { query: {} } as RouteLocationNormalized;
33-
const fromMock = {} as RouteLocationNormalized;
47+
it('should redirect to homepage if no redirect is set', async () => {
48+
const nextMock = vi.fn();
49+
const toMock = { query: {} } as RouteLocationNormalized;
50+
const fromMock = {} as RouteLocationNormalized;
3451

35-
await guestMiddleware(toMock, fromMock, nextMock, {});
52+
await guestMiddleware(toMock, fromMock, nextMock, {});
53+
54+
expect(nextMock).toHaveBeenCalledWith({ name: VIEWS.HOMEPAGE });
55+
});
56+
57+
it('should redirect to homepage if redirect is not a valid URL', async () => {
58+
const nextMock = vi.fn();
59+
const toMock = {
60+
query: { redirect: 'not-valid-url' },
61+
} as unknown as RouteLocationNormalized;
62+
63+
const fromMock = {} as RouteLocationNormalized;
64+
65+
await guestMiddleware(toMock, fromMock, nextMock, {});
66+
67+
expect(nextMock).toHaveBeenCalledWith({ name: VIEWS.HOMEPAGE });
68+
});
69+
70+
it('should redirect to homepage if redirect is not the origin domain', async () => {
71+
const nextMock = vi.fn();
72+
const toMock = {
73+
query: { redirect: 'https://n8n.local.evil.com/some-path' },
74+
} as unknown as RouteLocationNormalized;
75+
76+
const fromMock = {} as RouteLocationNormalized;
77+
78+
await guestMiddleware(toMock, fromMock, nextMock, {});
3679

37-
expect(nextMock).toHaveBeenCalledWith({ name: VIEWS.HOMEPAGE });
80+
expect(nextMock).toHaveBeenCalledWith({ name: VIEWS.HOMEPAGE });
81+
});
3882
});
3983

40-
it('should allow navigation if no current user is present', async () => {
84+
it('should not redirect if no current user is present', async () => {
4185
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType<
4286
typeof useUsersStore
4387
>);
@@ -48,7 +92,7 @@ describe('Middleware', () => {
4892

4993
await guestMiddleware(toMock, fromMock, nextMock, {});
5094

51-
expect(nextMock).toHaveBeenCalledTimes(0);
95+
expect(nextMock).not.toHaveBeenCalled();
5296
});
5397
});
5498
});

packages/frontend/editor-ui/src/utils/rbac/middleware/guest.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,24 @@ export const guestMiddleware: RouterMiddleware<GuestPermissionOptions> = async (
1010
) => {
1111
const valid = isGuest();
1212
if (!valid) {
13-
const redirect = to.query.redirect as string;
14-
if (redirect && (redirect.startsWith('/') || redirect.startsWith(window.location.origin))) {
13+
const redirect = (to.query.redirect as string) ?? '';
14+
15+
// Allow local path redirects
16+
if (redirect.startsWith('/')) {
1517
return next(redirect);
1618
}
1719

20+
try {
21+
// Only allow origin domain redirects
22+
const url = new URL(redirect);
23+
if (url.origin === window.location.origin) {
24+
return next(redirect);
25+
}
26+
} catch {
27+
// Intentionally fall through to redirect to homepage
28+
// if the redirect is an invalid URL
29+
}
30+
1831
return next({ name: VIEWS.HOMEPAGE });
1932
}
2033
};

0 commit comments

Comments
 (0)