Skip to content

Commit 535905c

Browse files
security: Stop automatically adding URLs from server-side load fetch calls to dependencies (#9945)
* feat: Add `dangerZone` config * breaking: Don't implicitly track deps in server-side fetch * changeset * bein dumb * fix: Server load invalidation * fix: Write config for server * fix: test * unsurprisingly i am dumb * docs: Clarify difference between server `fetch` and universal `fetch` * tests * rename to trackServerFetches --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 8572651 commit 535905c

File tree

18 files changed

+94
-12
lines changed

18 files changed

+94
-12
lines changed

.changeset/shaggy-moons-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
security: Stop implicitly tracking URLs as dependencies in server-side `load`s

documentation/docs/20-core-concepts/20-load.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ Dependency tracking does not apply _after_ the `load` function has returned —
537537

538538
### Manual invalidation
539539

540-
You can also re-run `load` functions that apply to the current page using [`invalidate(url)`](modules#$app-navigation-invalidate), which re-runs all `load` functions that depend on `url`, and [`invalidateAll()`](modules#$app-navigation-invalidateall), which re-runs every `load` function.
540+
You can also re-run `load` functions that apply to the current page using [`invalidate(url)`](modules#$app-navigation-invalidate), which re-runs all `load` functions that depend on `url`, and [`invalidateAll()`](modules#$app-navigation-invalidateall), which re-runs every `load` function. Server load functions will never automatically depend on a fetched `url` to avoid leaking secrets to the client.
541541

542542
A `load` function depends on `url` if it calls `fetch(url)` or `depends(url)`. Note that `url` can be a custom identifier that starts with `[a-z]:`:
543543

@@ -585,7 +585,7 @@ To summarize, a `load` function will re-run in the following situations:
585585
- It references a property of `params` whose value has changed
586586
- It references a property of `url` (such as `url.pathname` or `url.search`) whose value has changed. Properties in `request.url` are _not_ tracked
587587
- It calls `await parent()` and a parent `load` function re-ran
588-
- It declared a dependency on a specific URL via [`fetch`](#making-fetch-requests) or [`depends`](types#public-types-loadevent), and that URL was marked invalid with [`invalidate(url)`](modules#$app-navigation-invalidate)
588+
- It declared a dependency on a specific URL via [`fetch`](#making-fetch-requests) (universal load only) or [`depends`](types#public-types-loadevent), and that URL was marked invalid with [`invalidate(url)`](modules#$app-navigation-invalidate)
589589
- All active `load` functions were forcibly re-run with [`invalidateAll()`](modules#$app-navigation-invalidateall)
590590

591591
`params` and `url` can change in response to a `<a href="..">` link click, a [`<form>` interaction](form-actions#get-vs-post), a [`goto`](modules#$app-navigation-goto) invocation, or a [`redirect`](modules#sveltejs-kit-redirect).

packages/kit/src/core/config/index.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ const get_defaults = (prefix = '') => ({
6969
csrf: {
7070
checkOrigin: true
7171
},
72+
dangerZone: {
73+
trackServerFetches: false
74+
},
7275
embedded: false,
7376
env: {
7477
dir: process.cwd(),

packages/kit/src/core/config/options.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ const options = object(
111111
checkOrigin: boolean(true)
112112
}),
113113

114+
dangerZone: object({
115+
// TODO 2.0: Remove this
116+
trackServerFetches: boolean(false)
117+
}),
118+
114119
embedded: boolean(false),
115120

116121
env: object({

packages/kit/src/core/sync/write_server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const options = {
3434
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
3535
csp: ${s(config.kit.csp)},
3636
csrf_check_origin: ${s(config.kit.csrf.checkOrigin)},
37+
track_server_fetches: ${s(config.kit.dangerZone.trackServerFetches)},
3738
embedded: ${config.kit.embedded},
3839
env_public_prefix: '${config.kit.env.publicPrefix}',
3940
hooks: null, // added lazily, via \`get_hooks\`

packages/kit/src/runtime/server/data/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export async function render_data(
7676
}
7777
}
7878
return data;
79-
}
79+
},
80+
track_server_fetches: options.track_server_fetches
8081
});
8182
} catch (e) {
8283
aborted = true;

packages/kit/src/runtime/server/page/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ export async function render_page(event, page, options, manifest, state, resolve
150150
if (parent) Object.assign(data, await parent.data);
151151
}
152152
return data;
153-
}
153+
},
154+
track_server_fetches: options.track_server_fetches
154155
});
155156
} catch (e) {
156157
load_error = /** @type {Error} */ (e);

packages/kit/src/runtime/server/page/load_data.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ import { validate_depends } from '../../shared.js';
1010
* state: import('types').SSRState;
1111
* node: import('types').SSRNode | undefined;
1212
* parent: () => Promise<Record<string, any>>;
13+
* track_server_fetches: boolean;
1314
* }} opts
1415
* @returns {Promise<import('types').ServerDataNode | null>}
1516
*/
16-
export async function load_server_data({ event, state, node, parent }) {
17+
export async function load_server_data({
18+
event,
19+
state,
20+
node,
21+
parent,
22+
// TODO 2.0: Remove this
23+
track_server_fetches
24+
}) {
1725
if (!node?.server) return null;
1826

1927
let done = false;
@@ -51,7 +59,10 @@ export async function load_server_data({ event, state, node, parent }) {
5159
);
5260
}
5361

54-
uses.dependencies.add(url.href);
62+
// TODO 2.0: Remove this
63+
if (track_server_fetches) {
64+
uses.dependencies.add(url.href);
65+
}
5566

5667
return event.fetch(info, init);
5768
},

packages/kit/src/runtime/server/page/respond_with_error.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export async function respond_with_error({
4444
event,
4545
state,
4646
node: default_layout,
47-
parent: async () => ({})
47+
parent: async () => ({}),
48+
track_server_fetches: options.track_server_fetches
4849
});
4950

5051
const server_data = await server_data_promise;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,14 +456,15 @@ test.describe('Invalidation', () => {
456456
expect(shared).not.toBe(next_shared);
457457
});
458458

459-
test('fetch in server load can be invalidated', async ({ page, app, request }) => {
459+
test('fetch in server load cannot be invalidated', async ({ page, app, request }) => {
460+
// TODO 2.0: Can remove this test after `dangerZone.trackServerFetches` and associated code is removed
460461
await request.get('/load/invalidation/server-fetch/count.json?reset');
461462
await page.goto('/load/invalidation/server-fetch');
462463
const selector = '[data-testid="count"]';
463464

464465
expect(await page.textContent(selector)).toBe('1');
465466
await app.invalidate('/load/invalidation/server-fetch/count.json');
466-
expect(await page.textContent(selector)).toBe('2');
467+
expect(await page.textContent(selector)).toBe('1');
467468
});
468469

469470
test('+layout.js is re-run when shared dep is invalidated', async ({ page }) => {

0 commit comments

Comments
 (0)