eyecatch

Sveltekit - Storeからの情報漏洩の可能性について

Posted on 2024/08/01
# Technology

はじめに

サーバーでは state の共有を避ける


ブラウザは state を保持します(Browsers are stateful) — ユーザーがアプリケーションとやりとりする際に、state はメモリ内に保存されます。一方、サーバーは state を保持しません(Servers are stateless) — レスポンスの内容は、完全にリクエストの内容によって決定されます。

概念としては、そうです。現実では、サーバーは長い期間存在し、複数のユーザーで共有されることが多いです。そのため、共有される変数にデータを保存しないことが重要です。

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

公式サイトを見てるとこのように State をサーバー側では共有しないように注意しています。Long-lived なサーバーではメモリーに保存した値がそのまま残ってしまうからです。
実際どのようにしたらデータが共有され、情報漏洩の危険になってしまうかを検証したいと思います。

本来はGlobalなState管理に import { writable } from 'svelte/store'; というAPIが提供されていますが、せっかくなので今回は Svelte 5のRuneを使って記述していきたいと思います。

見た目は sveltekitをinitした状態を少し触ったものを使用します。またスタイルなどは今回触れません。

Storeを Rune で作ってみる

早速 Store を作ってみましょう。

// counterStore.svelte.ts

let count = $state(0);

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

簡単ですね。 このファイルを読み出すと let count = $state(0); が memory上に保存され global的にアクセスできるようになります。

試しに ブラウザ側で呼び出してみます。Browser側で couterStore を呼び出す用の CounterBrowser.svelte を用意して store を呼び出します。

// 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>

2つのブラウザを開いて挙動を確認すると片方のみグローバルで値が反映されてます。コンポネントを2回表示してグローバルなstoreであることを確かめます。しかし値はブラウザーのメモリに格納されるのでもう片方のブラウザーには反映されません。

共有された State の検証

では実際にこの Store State を Server side で stateが共有される形で使用してみます。
State の更新は Server側で行いたいので sveltekit の form action を使用して更新を試みる形にします。

// +page.server.ts

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

export const load = async () => {
        // Server側で store を呼び出す
	const counter = createCounter();

	return {
                // フロント側に値を渡す
		count: counter.count
	};
};

// Store の更新は form action を使う
export const actions = {
	increment: async () => {
		const counter = createCounter();
		counter.increment();
		return {};
	},
	decrement: async () => {
		const counter = createCounter();
		counter.decrement();
		return {};
	}
} satisfies Actions;

Form action を実行するために先ほどの CounterBrowser.svelte を少し改良して 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>

各+-のボタンをクリックするとそれぞれの form action をリクエストします。Form actionが実行されると自動的に load functionが再実行されて、値の更新がされます。このコンポネントを +page.svelteで呼びます。(余分なコードは消してます)

// +page.svelte

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

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

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

では実際に Sessionが違うブラウザーを2つ開いて挙動を見てみます。

更新後、別のセッションでページを更新すると値が更新されます。Stateが共有されていることがわかりました。

これは 別セッションのブラウザでも Server は共有されたものを使用してるからです。
使い方に気をつけなければユーザーなどの個人情報が漏洩される危険があります。

まとめ

SvelteのState管理は非常に強力で使いやすいですが、SvelteKitのSSR時などは十分に気をつける必要があります。
Log-livedなServerではStateが共有されてしまうので、お客様の情報が他人に漏洩してしまう可能性があります。
対策として Svelteから Context API が提供されてるので、 User Id を key に storeを保存する方法などがありますが、公式通りに使わない方が安全かもしれません。