Skip to content

Commit dab9817

Browse files
dummdidummRich-Harrisbenmccann
authored
breaking: disallow external navigation using goto (#11207)
* breaking: disallow external navigation using `goto` by default closes #8775 * do it here instead * adjust test * remove option, TODO tests * tweak message * Update .changeset/silent-games-taste.md * add a test, remove an obsolete test * fix test * prettier * fix * Update packages/kit/src/exports/public.d.ts Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * Update packages/kit/src/runtime/client/client.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * fix test * DRY out --------- Co-authored-by: Rich Harris <rich.harris@vercel.com> Co-authored-by: Rich Harris <richard.a.harris@gmail.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
1 parent 8e7cfef commit dab9817

File tree

8 files changed

+66
-30
lines changed

8 files changed

+66
-30
lines changed

.changeset/silent-games-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': major
3+
---
4+
5+
breaking: disallow external navigation with `goto`

packages/kit/src/exports/public.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ export interface Navigation {
846846
/**
847847
* The type of navigation:
848848
* - `form`: The user submitted a `<form>`
849-
* - `leave`: The user is leaving the app by closing the tab or using the back/forward buttons to go to a different document
849+
* - `leave`: The app is being left either because the tab is being closed or a navigation to a different document is occurring
850850
* - `link`: Navigation was triggered by a link click
851851
* - `goto`: Navigation was triggered by a `goto(...)` call or a redirect
852852
* - `popstate`: Navigation was triggered by back/forward navigation

packages/kit/src/runtime/app/navigation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scro
2323
* @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
2424
* @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation
2525
* @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body
26-
* @param {boolean} [invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation.
26+
* @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation.
2727
* @param {any} [opts.state] The state of the new/updated history entry
2828
* @returns {Promise<void>}
2929
*/

packages/kit/src/runtime/client/client.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { parse } from './parse.js';
1818
import * as storage from './session-storage.js';
1919
import {
2020
find_anchor,
21-
get_base_uri,
21+
resolve_url,
2222
get_link_info,
2323
get_router_options,
2424
is_external_url,
@@ -235,12 +235,8 @@ export function create_client(app, target) {
235235
redirect_count,
236236
nav_token
237237
) {
238-
if (typeof url === 'string') {
239-
url = new URL(url, get_base_uri(document));
240-
}
241-
242238
return navigate({
243-
url,
239+
url: resolve_url(url),
244240
scroll: noScroll ? scroll_state() : null,
245241
keepfocus: keepFocus,
246242
redirect_count,
@@ -1375,8 +1371,20 @@ export function create_client(app, target) {
13751371
}
13761372
},
13771373

1378-
goto: (href, opts = {}) => {
1379-
return goto(href, opts, 0);
1374+
goto: (url, opts = {}) => {
1375+
url = resolve_url(url);
1376+
1377+
if (url.origin !== origin) {
1378+
return Promise.reject(
1379+
new Error(
1380+
DEV
1381+
? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
1382+
: 'goto: invalid URL'
1383+
)
1384+
);
1385+
}
1386+
1387+
return goto(url, opts, 0);
13801388
},
13811389

13821390
invalidate: (resource) => {
@@ -1396,7 +1404,7 @@ export function create_client(app, target) {
13961404
},
13971405

13981406
preload_data: async (href) => {
1399-
const url = new URL(href, get_base_uri(document));
1407+
const url = resolve_url(href);
14001408
const intent = get_navigation_intent(url, false);
14011409

14021410
if (!intent) {

packages/kit/src/runtime/client/utils.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import { PRELOAD_PRIORITIES } from './constants.js';
88

99
export const origin = BROWSER ? location.origin : '';
1010

11-
/** @param {HTMLDocument} doc */
12-
export function get_base_uri(doc) {
13-
let baseURI = doc.baseURI;
11+
/** @param {string | URL} url */
12+
export function resolve_url(url) {
13+
if (url instanceof URL) return url;
14+
15+
let baseURI = document.baseURI;
1416

1517
if (!baseURI) {
16-
const baseTags = doc.getElementsByTagName('base');
17-
baseURI = baseTags.length ? baseTags[0].href : doc.URL;
18+
const baseTags = document.getElementsByTagName('base');
19+
baseURI = baseTags.length ? baseTags[0].href : document.URL;
1820
}
1921

20-
return baseURI;
22+
return new URL(url, baseURI);
2123
}
2224

2325
export function scroll_state() {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script>
2+
import { goto } from '$app/navigation';
3+
4+
let message = '...';
5+
</script>
6+
7+
<button
8+
on:click={async () => {
9+
try {
10+
await goto('https://example.com');
11+
} catch (e) {
12+
message = e.message;
13+
}
14+
}}>goto</button
15+
>
16+
17+
<p>{message}</p>

packages/kit/test/apps/basics/test/client.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,3 +841,15 @@ test.describe('Assets', () => {
841841
).toBe(true);
842842
});
843843
});
844+
845+
test.describe('goto', () => {
846+
test('goto fails with external URL', async ({ page }) => {
847+
await page.goto('/goto');
848+
await page.click('button');
849+
850+
const message = process.env.DEV
851+
? 'Cannot use `goto` with an external URL. Use `window.location = "https://example.com/"` instead'
852+
: 'goto: invalid URL';
853+
await expect(page.locator('p')).toHaveText(message);
854+
});
855+
});

packages/kit/test/apps/basics/test/cross-platform/client.test.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,6 @@ test.describe('Navigation lifecycle functions', () => {
141141
expect(await page.innerHTML('pre')).toBe('1 false goto');
142142
});
143143

144-
test('beforeNavigate prevents external navigation triggered by goto', async ({
145-
page,
146-
app,
147-
baseURL
148-
}) => {
149-
await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation');
150-
await app.goto('https://google.de');
151-
expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation');
152-
expect(await page.innerHTML('pre')).toBe('1 true goto');
153-
});
154-
155144
test('beforeNavigate prevents navigation triggered by back button', async ({
156145
page,
157146
app,
@@ -215,8 +204,11 @@ test.describe('Navigation lifecycle functions', () => {
215204
}) => {
216205
await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation');
217206
await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture.
218-
219-
await app.goto('https://google.de');
207+
page.on('dialog', async (dialog) => {
208+
await dialog.dismiss();
209+
});
210+
await page.close({ runBeforeUnload: true });
211+
await page.waitForTimeout(100);
220212
await app.goto('/navigation-lifecycle/before-navigate/prevent-navigation?x=1');
221213

222214
expect(await page.innerHTML('pre')).toBe('2 false goto');

0 commit comments

Comments
 (0)