SvelteKit 本番運用チェックリスト:CSP/SRI/Cache-Control/Headers 総点検
SvelteKit でアプリケーションを開発し、いざ本番環境へリリース。しかし、セキュリティやパフォーマンスの設定を見落としていませんか?
本番運用では、Content Security Policy(CSP)、Subresource Integrity(SRI)、Cache-Control、HTTP ヘッダーなど、多岐にわたる設定が求められます。これらを適切に構成することで、XSS 攻撃の防止、改ざん検知、キャッシュ効率化、セキュリティヘッダーによる脅威軽減が実現できるのです。
本記事では、SvelteKit アプリケーションを本番環境で安全かつ高速に運用するために必要な設定項目を総点検します。初心者の方でも実践できるよう、各項目の背景・課題・解決策・具体例を段階的に解説していきますね。
背景
Web アプリケーションのセキュリティとパフォーマンスの重要性
Web アプリケーションは、リリース後も常に外部からの攻撃やパフォーマンス低下のリスクにさらされています。特に以下のような脅威が存在します。
| # | 脅威 | 影響 |
|---|---|---|
| 1 | XSS(クロスサイトスクリプティング) | 悪意のあるスクリプトが実行され、ユーザー情報が窃取される |
| 2 | CDN や外部リソースの改ざん | 第三者による不正なコードの挿入 |
| 3 | キャッシュ設定の不備 | サーバー負荷増大、ページ表示速度の低下 |
| 4 | セキュリティヘッダーの欠如 | クリックジャッキング、MIME タイプスニッフィングなどの攻撃 |
これらのリスクに対処するため、本番環境では複数のセキュリティ層を設ける必要があります。
以下の図は、SvelteKit アプリケーションにおけるセキュリティとパフォーマンスの全体像を示しています。
mermaidflowchart TB
user["ユーザー"]
browser["ブラウザ"]
server["SvelteKit<br/>サーバー"]
cdn["CDN/<br/>外部リソース"]
user -->|アクセス| browser
browser -->|リクエスト| server
server -->|セキュリティヘッダー<br/>CSP, Cache-Control| browser
browser -->|外部リソース<br/>取得| cdn
cdn -->|JS/CSS<br/>SRI検証| browser
browser -->|レンダリング<br/>実行| user
この図のように、サーバーからブラウザへ送信される HTTP ヘッダー、外部リソースの整合性検証、キャッシュ制御が連携して、セキュリティとパフォーマンスを実現します。
図で理解できる要点:
- サーバーは HTTP ヘッダーを通じてブラウザに指示を出す
- 外部リソースは SRI によって整合性が検証される
- 複数のセキュリティ層が協調して動作する
SvelteKit の本番環境特有の考慮事項
SvelteKit は、SSR(Server-Side Rendering)、CSR(Client-Side Rendering)、SSG(Static Site Generation)を柔軟に組み合わせられるフレームワークです。この柔軟性ゆえに、本番環境では以下の点に注意が必要です。
- adapter による環境差異: adapter-node、adapter-vercel など、デプロイ先によって設定方法が異なる
- 動的レンダリングと静的生成の混在: ページごとに異なるキャッシュ戦略が必要
- CSP と nonce の管理: SSR 時に動的に nonce を生成し、インラインスクリプトを許可する仕組みが必要
これらの特性を理解したうえで、適切な設定を行うことが本番運用の成功につながります。
課題
セキュリティ設定の複雑さ
SvelteKit アプリケーションを本番環境で運用する際、以下のような課題に直面します。
1. CSP(Content Security Policy)の設定が難しい
CSP は、許可されたリソースのみを読み込むようブラウザに指示するセキュリティヘッダーです。しかし、以下の理由で設定が複雑になりがちです。
- インラインスクリプトやスタイルをどう許可するか
- 外部 CDN や Google Fonts などのリソースをどう扱うか
- nonce や hash の管理方法
誤った設定をすると、アプリケーションが正常に動作しなくなる可能性があります。
2. SRI(Subresource Integrity)の実装が手間
SRI は、外部リソースが改ざんされていないかをハッシュ値で検証する仕組みです。しかし、以下の手間がかかります。
- CDN から読み込む各リソースのハッシュ値を生成
- バージョンアップ時にハッシュ値を更新
- ビルド時に自動生成する仕組みの構築
3. Cache-Control の最適化が不明確
キャッシュ戦略は、パフォーマンスに直結します。しかし、以下の疑問が生じます。
- 静的アセット(JS、CSS、画像)にどのくらいの有効期限を設定すべきか
- HTML ページはキャッシュすべきか、毎回サーバーに問い合わせるべきか
- API レスポンスのキャッシュ戦略は?
4. セキュリティヘッダーの漏れ
以下のようなセキュリティヘッダーを設定し忘れると、脆弱性が残ります。
| # | ヘッダー | 目的 |
|---|---|---|
| 1 | X-Frame-Options | クリックジャッキング防止 |
| 2 | X-Content-Type-Options | MIME タイプスニッフィング防止 |
| 3 | Referrer-Policy | リファラー情報の制御 |
| 4 | Permissions-Policy | ブラウザ機能の制限 |
以下の図は、これらの課題がどのように関連しているかを示しています。
mermaidflowchart LR
csp["CSP設定<br/>複雑"]
sri["SRI実装<br/>手間"]
cache["Cache-Control<br/>不明確"]
headers["セキュリティヘッダー<br/>漏れ"]
risk["セキュリティリスク<br/>パフォーマンス低下"]
csp --> risk
sri --> risk
cache --> risk
headers --> risk
これらの課題を放置すると、セキュリティリスクやパフォーマンス低下につながります。次のセクションでは、これらを解決する具体的な方法を見ていきましょう。
解決策
1. CSP(Content Security Policy)の設定
CSP を適切に設定することで、XSS 攻撃を効果的に防げます。SvelteKit では、hooks.server.ts で HTTP ヘッダーを設定するのが基本です。
CSP の基本方針
以下の方針で CSP を設定します。
- default-src: デフォルトで自分のドメインのみ許可
- script-src: nonce を使ってインラインスクリプトを許可
- style-src: nonce またはハッシュでインラインスタイルを許可
- img-src / font-src: 必要な外部ドメインを明示的に許可
nonce の生成と適用
nonce は、リクエストごとに一意のランダム値を生成し、許可されたスクリプトやスタイルにのみ付与します。
以下の図は、CSP と nonce の動作フローを示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant Server as SvelteKit<br/>サーバー
participant Browser as ブラウザ
User->>Server: ページリクエスト
Server->>Server: nonce生成
Server->>Browser: HTML + CSP header<br/>(nonce含む)
Browser->>Browser: CSP検証<br/>nonce一致確認
Browser->>User: ページ表示
このように、サーバーで生成された nonce がブラウザで検証されることで、許可されたスクリプトのみが実行されます。
2. SRI(Subresource Integrity)の実装
SRI は、外部リソースの整合性を検証するための仕組みです。CDN から読み込む JavaScript や CSS に対して、ハッシュ値を指定します。
SRI の実装方針
- ビルド時にハッシュ値を自動生成: Vite プラグインや専用ツールを活用
- CDN リソースには手動でハッシュ値を設定: 公式サイトから取得
- クロスオリジンリソースには
crossorigin属性を追加
3. Cache-Control の最適化
キャッシュ戦略は、リソースの種類によって使い分けます。
| # | リソース | Cache-Control | 理由 |
|---|---|---|---|
| 1 | 静的アセット(JS/CSS) | public, max-age=31536000, immutable | ハッシュ付きファイル名なので長期キャッシュ可能 |
| 2 | HTML ページ | no-cache または max-age=0 | 常に最新版を取得 |
| 3 | API レスポンス | private, max-age=60 | ユーザーごとに異なるデータ |
| 4 | 画像 | public, max-age=86400 | 1 日程度のキャッシュ |
キャッシュ戦略の図解
以下の図は、リソースごとのキャッシュフローを示しています。
mermaidflowchart TD
request["リクエスト"]
check["キャッシュ確認"]
cached["キャッシュ有効"]
expired["キャッシュ期限切れ"]
serve["キャッシュから<br/>返却"]
fetch["サーバーへ<br/>リクエスト"]
update["キャッシュ<br/>更新"]
request --> check
check --> cached
check --> expired
cached --> serve
expired --> fetch
fetch --> update
update --> serve
この仕組みにより、有効なキャッシュがあれば即座に返却し、期限切れの場合のみサーバーへリクエストすることで、パフォーマンスが向上します。
4. セキュリティヘッダーの設定
以下のセキュリティヘッダーを設定することで、多層的な防御を実現します。
| # | ヘッダー | 推奨値 | 効果 |
|---|---|---|---|
| 1 | X-Frame-Options | DENY または SAMEORIGIN | iframe への埋め込みを制限 |
| 2 | X-Content-Type-Options | nosniff | MIME タイプの推測を防止 |
| 3 | Referrer-Policy | strict-origin-when-cross-origin | リファラー情報の制御 |
| 4 | Permissions-Policy | geolocation=(), microphone=() | 不要な機能を無効化 |
| 5 | Strict-Transport-Security | max-age=31536000; includeSubDomains | HTTPS 強制 |
これらのヘッダーを hooks.server.ts で一括設定することで、セキュリティを強化できます。
具体例
完全な hooks.server.ts の実装
ここでは、CSP、SRI、Cache-Control、セキュリティヘッダーをすべて設定した hooks.server.ts の実装例を段階的に解説します。
ステップ 1: nonce 生成関数の実装
リクエストごとに一意の nonce を生成します。
typescript// src/hooks.server.ts
import { randomBytes } from 'crypto';
/**
* CSP nonce を生成する関数
* ランダムな16バイトをBase64エンコードして返します
*/
function generateNonce(): string {
return randomBytes(16).toString('base64');
}
randomBytes を使って暗号学的に安全なランダム値を生成し、Base64 エンコードすることで nonce として利用できる形式にします。
ステップ 2: CSP ヘッダーの構築
nonce を含む CSP ヘッダーを生成します。
typescript/**
* CSP ヘッダーを構築する関数
* nonce を使ってインラインスクリプトとスタイルを許可します
*/
function buildCSP(nonce: string): string {
const directives = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' https://cdn.example.com`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
`font-src 'self' https://fonts.gstatic.com`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
];
return directives.join('; ');
}
各ディレクティブで、許可するリソースの取得元を明示的に指定しています。nonce-${nonce} により、サーバーが生成した nonce を持つスクリプトとスタイルのみが実行されます。
ステップ 3: セキュリティヘッダーの設定
複数のセキュリティヘッダーをまとめて設定します。
typescript/**
* セキュリティヘッダーを設定する関数
* CSP以外の主要なセキュリティヘッダーを返します
*/
function getSecurityHeaders(): Record<string, string> {
return {
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy':
'geolocation=(), microphone=(), camera=()',
'Strict-Transport-Security':
'max-age=31536000; includeSubDomains; preload',
};
}
これらのヘッダーにより、クリックジャッキング、MIME スニッフィング、不要なブラウザ機能の利用などを防ぎます。
ステップ 4: Cache-Control の設定
リソースの種類に応じて Cache-Control を設定します。
typescript/**
* リソースタイプに応じた Cache-Control を返す関数
* パスから適切なキャッシュ戦略を判定します
*/
function getCacheControl(pathname: string): string {
// 静的アセット(ハッシュ付きファイル名)
if (pathname.match(/\/_app\/immutable\//)) {
return 'public, max-age=31536000, immutable';
}
// 画像ファイル
if (pathname.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)) {
return 'public, max-age=86400';
}
// HTMLページ
if (
pathname.endsWith('.html') ||
!pathname.includes('.')
) {
return 'no-cache';
}
// その他の静的ファイル
return 'public, max-age=3600';
}
SvelteKit は、ビルド時に _app/immutable/ ディレクトリ配下に内容ハッシュを含むファイル名で静的アセットを出力します。これらは内容が変わらない限りファイル名も変わらないため、長期キャッシュが可能です。
ステップ 5: handle フックの実装
すべてのリクエストに対してヘッダーを設定します。
typescriptimport type { Handle } from '@sveltejs/kit';
/**
* SvelteKit の handle フック
* すべてのリクエストに対してセキュリティヘッダーを設定します
*/
export const handle: Handle = async ({ event, resolve }) => {
// nonce を生成
const nonce = generateNonce();
// nonce をローカル変数に保存(テンプレートで使用可能)
event.locals.nonce = nonce;
// レスポンスを取得
const response = await resolve(event, {
transformPageChunk: ({ html }) => {
// HTML内のプレースホルダーをnonceに置換
return html.replace('%sveltekit.nonce%', nonce);
}
});
transformPageChunk を使うことで、HTML 内の %sveltekit.nonce% プレースホルダーを実際の nonce 値に置換できます。
ステップ 6: レスポンスヘッダーの設定
生成された nonce と各種ヘッダーをレスポンスに追加します。
typescript // CSPヘッダーを設定
response.headers.set('Content-Security-Policy', buildCSP(nonce));
// その他のセキュリティヘッダーを設定
const securityHeaders = getSecurityHeaders();
Object.entries(securityHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
// Cache-Controlを設定
const cacheControl = getCacheControl(event.url.pathname);
response.headers.set('Cache-Control', cacheControl);
return response;
};
すべてのセキュリティヘッダーとキャッシュ制御ヘッダーをレスポンスに追加して返します。
ステップ 7: 型定義の追加
TypeScript で event.locals.nonce を使えるよう型定義を追加します。
typescript// src/app.d.ts
declare global {
namespace App {
interface Locals {
nonce: string;
}
}
}
export {};
これにより、event.locals.nonce が型安全に利用できます。
HTML テンプレートでの nonce 使用
src/app.html で nonce を使ってインラインスクリプトを許可します。
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<link
rel="icon"
href="%sveltekit.assets%/favicon.png"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<!-- Google Fonts の読み込み -->
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
/>
%sveltekit.head%
</head>
</html>
ヘッダー部分で、外部フォントの preconnect を設定しています。CSP で許可したドメインと一致させることが重要です。
html<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<!-- インラインスクリプトにnonceを付与 -->
<script nonce="%sveltekit.nonce%">
// 初期化処理など
console.log('App initialized');
</script>
</body>
</html>
%sveltekit.nonce% が transformPageChunk で実際の nonce 値に置換され、CSP で許可されたスクリプトとして実行されます。
SRI の実装例
外部 CDN から読み込むリソースに SRI を適用します。
html<!-- jQuery を CDN から SRI 付きで読み込む -->
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs"
crossorigin="anonymous"
></script>
integrity 属性にハッシュ値を指定し、crossorigin 属性を追加することで、SRI が有効になります。
html<!-- Bootstrap CSS を CDN から SRI 付きで読み込む -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
/>
CSS ファイルにも同様に SRI を適用できます。ハッシュ値は、CDN の公式サイトや SRI Hash Generator などのツールで取得できますよ。
ビルド時の SRI 自動生成(Vite プラグイン)
Vite プラグインを使うことで、ビルド時に自動的に SRI ハッシュを生成できます。
typescript// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
/**
* Vite の設定
* SRI プラグインを追加してビルド時にハッシュを生成します
*/
export default defineConfig({
plugins: [
sveltekit(),
// SRI プラグイン(例: vite-plugin-sri)
],
});
プラグインの具体的な実装は、vite-plugin-sri などのパッケージを利用することで簡単に導入できます。
API ルートでのキャッシュ制御
API エンドポイントでも、レスポンスごとに適切な Cache-Control を設定します。
typescript// src/routes/api/users/+server.ts
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
/**
* ユーザー一覧を返すAPIエンドポイント
* プライベートキャッシュを60秒間有効にします
*/
export const GET: RequestHandler = async () => {
const users = [
{ id: 1, name: '田中太郎' },
{ id: 2, name: '佐藤花子' },
];
return json(users, {
headers: {
'Cache-Control': 'private, max-age=60',
},
});
};
API レスポンスには private を指定し、ユーザーごとに異なるデータであることを明示します。共有キャッシュ(CDN など)には保存されず、ブラウザのみがキャッシュします。
環境変数による CSP の切り替え
開発環境と本番環境で CSP の厳格さを変えることも可能です。
typescript// src/hooks.server.ts
import { dev } from '$app/environment';
/**
* 環境に応じた CSP を構築
* 開発環境では制限を緩和します
*/
function buildCSP(nonce: string): string {
if (dev) {
// 開発環境: 制限を緩和
return `default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self' 'unsafe-inline' 'unsafe-eval'`;
}
// 本番環境: 厳格な設定
const directives = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
// ...
];
return directives.join('; ');
}
開発中は HMR(Hot Module Replacement)などで unsafe-inline や unsafe-eval が必要になる場合があります。本番環境では厳格な CSP を適用しましょう。
デプロイ先別の設定例
Vercel でのヘッダー設定
Vercel では、vercel.json でもヘッダーを設定できます。
json{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
]
}
ただし、動的な nonce を含む CSP は hooks.server.ts で設定する必要があります。
Nginx でのヘッダー設定
Nginx を使う場合は、設定ファイルでヘッダーを追加できます。
nginx# /etc/nginx/sites-available/myapp
server {
listen 443 ssl http2;
server_name example.com;
# セキュリティヘッダー
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 静的アセットのキャッシュ
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SvelteKit アプリケーションへプロキシ
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Nginx 層でヘッダーを設定することで、SvelteKit アプリケーションの負荷を軽減できます。
まとめ
本記事では、SvelteKit アプリケーションを本番環境で安全かつ高速に運用するための設定を総点検しました。
CSP(Content Security Policy) は、nonce を使ってインラインスクリプトを安全に許可し、XSS 攻撃を防ぎます。hooks.server.ts でリクエストごとに一意の nonce を生成し、HTML テンプレートに埋め込むことで実現できるのです。
SRI(Subresource Integrity) は、外部 CDN から読み込むリソースが改ざんされていないことをハッシュ値で検証します。integrity 属性と crossorigin 属性を追加するだけで、簡単に導入できますね。
Cache-Control は、リソースの種類に応じて適切なキャッシュ戦略を設定します。静的アセットは長期キャッシュ、HTML ページは常に最新版を取得、API レスポンスはプライベートキャッシュといった使い分けが重要です。
セキュリティヘッダー は、多層的な防御を実現します。X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy、Strict-Transport-Security を設定することで、さまざまな攻撃から守れるでしょう。
これらの設定を適切に行うことで、SvelteKit アプリケーションのセキュリティとパフォーマンスが大幅に向上します。本記事のチェックリストを参考に、ぜひ本番環境の設定を見直してみてくださいね。
関連リンク
articleSvelteKit 本番運用チェックリスト:CSP/SRI/Cache-Control/Headers 総点検
articleSvelte フォーム体験設計:Optimistic UI/エラー復旧/再送戦略の型
articleSvelte を macOS + yarn + TypeScript で最短構築:ESLint/Prettier まで一気通貫
articleSvelte 旧リアクティブ記法 vs Runes:可読性・コード量・パフォーマンス比較
articleSvelte の Hydration Mismatch を根絶:原因 18 パターンと修正チェックリスト
articleSvelte 5 Runes 徹底解説:リアクティブの再設計と移行の勘所
articleRuby とは?2025 年版の特徴・強み・最新エコシステムを徹底解説
articlePHP とは?2025 年版の特徴・強み・できることを徹底解説【保存版】
articleJotai 運用ガイド:命名規約・debugLabel・依存グラフ可視化の標準化
articleZod vs Ajv/Joi/Valibot/Superstruct:DX・速度・サイズを本気でベンチ比較
articleYarn でモノレポ設計:パッケージ分割、共有ライブラリ、リリース戦略
articleJest を可観測化する:JUnit/SARIF/OpenTelemetry で CI ダッシュボードを構築
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来