eyecatch

SvelteKit - Potential Information Leakage from the State

Posted on 2024/08/03
# Technology

Introduction

Avoid shared state on the server


Browsers are stateful — state is stored in memory as the user interacts with the application. Servers, on the other hand, are stateless — the content of the response is determined entirely by the content of the request.

Conceptually, that is. In reality, servers are often long-lived and shared by multiple users.

(https://kit.svelte.jp/docs/state-management#avoid-shared-state-on-the-server)

According to the official website, it is important to be careful not to share the State on the server side. This is because, on long-lived servers, values stored in memory remain as they are. We would like to verify how data can be shared and how this can lead to the risk of information leakage.

Before Svelte 5, they provides import { writable } from 'svelte/store'; API to manage the state. For this time, let use Rune from Svelte 5 instead.

We use UI from SvelteKit default page which you can see when init SvelteKit project. But I wouldn't talk about any styles in this blog.

Create Store with Rune

Let's make a store with Rune from svelte 5.

// counterStore.svelte.ts

let count = $state(0);

export function createCounter() {
	return {
		get count() {
			return count;
		},
		increment: () => (count += 1),
		decrement: () => (count -= 1)
	};
}

Easy!! Bt importing this file let count = $state(0); will be store on memory globally. For testing, import this and check it on a browser.

Create CouterBrowser.svelte to call counterStore on the client side.

// CounterBrowser.svelte

<script lang="ts">
	import { createCounter } from '$lib/countStore.svelte';

	const counter = createCounter();
</script>

<div class="counter">
	<button onclick={counter.decrement}>
		<svg aria-hidden="true" viewBox="0 0 1 1">
			<path d="M0,0.5 L1,0.5" />
		</svg>
	</button>

	<div class="counter-viewport">
		<div class="counter-digits">
			<strong class="hidden" aria-hidden="true">{Math.floor(counter.count)}</strong>
			<strong>{Math.floor(counter.count)}</strong>
		</div>
	</div>

	<button onclick={counter.increment}>
		<svg aria-hidden="true" viewBox="0 0 1 1">
			<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
		</svg>
	</button>
</div>

Open two browsers which they have different sessions. You can see the store not affect to each other, it's because the state store is stored in memory on the browser.

Verification of Shared State

Now, let's actually use this Store State in a way that it is shared on the server side. Since we want to update the State on the server side, we will try to update it using SvelteKit's form actions.

// +page.server.ts

import { createCounter } from '$lib/countStore.svelte';
import type { Actions } from './$types';

export const load = async () => {
        // Call store on a server side
	const counter = createCounter();

	return {
                // Give store value to a client side
		count: counter.count
	};
};

// Use form action to update the store value
export const actions = {
	increment: async () => {
		const counter = createCounter();
		counter.increment();
		return {};
	},
	decrement: async () => {
		const counter = createCounter();
		counter.decrement();
		return {};
	}
} satisfies Actions;

To execute the form action, we will modify the previously mentioned CounterBrowser.svelte to create Counter.svelte.

// Counter.svelte

<script lang="ts">
	import { enhance } from '$app/forms';

	type Props = {
		count: number;
	};

	let { count }: Props = $props();
</script>

<div class="counter">
	<form method="POST" action="?/decrement" use:enhance>
		<button type="submit">
			<svg aria-hidden="true" viewBox="0 0 1 1">
				<path d="M0,0.5 L1,0.5" />
			</svg>
		</button>
	</form>

	<div class="counter-viewport">
		<div class="counter-digits">
			<strong class="hidden" aria-hidden="true">{Math.floor(count)}</strong>
			<strong>{Math.floor(count)}</strong>
		</div>
	</div>

	<form method="POST" action="?/increment" use:enhance>
		<button type="submit">
			<svg aria-hidden="true" viewBox="0 0 1 1">
				<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
			</svg>
		</button>
	</form>
</div>

Each + and - button click will request the respective form action. When the form action is executed, the load function will automatically re-run, updating the values. This component will be called in +page.svelte (with extraneous code removed).

// +page.svelte

<script>
	import Counter from './Counter.svelte';

	let { data } = $props();
</script>

<section>
	<Counter count={data.count} />
</section>

Now, let's actually open two browsers with different sessions and observe the behavior.

After updating, refreshing the page in a different session shows the updated value. This indicates that the State is being shared.

This happens because the server uses shared state even in different browser sessions. If not used carefully, there is a risk of leaking user or personal information.

Conclusion

State management in Svelte is very powerful and easy to use, but you need to be careful when using SvelteKit's SSR. On long-lived servers, the state may be shared, which could lead to customer information being leaked to others. As a countermeasure, Svelte provides a Context API, allowing you to store the state with the User ID as the key. However, it might be safer not to use it exactly as described in the official documentation.