対象: lumilumi (SvelteKit + Svelte 5 Nostrクライアント) — 415ソースファイル 監査日: 2026-03-21 監査観点: セキュリティ / バグ・ロジック / TypeScript品質 / アーキテクチャ / パフォーマンス・メモリ 監査方法: 5つの専門エージェントによる並列静的解析
| 深刻度 | 件数 |
|---|---|
| CRITICAL | 7 |
| HIGH | 18 |
| MEDIUM | 21 |
| LOW | 11 |
| 合計 | 57 |
最も緊急性が高いのはサーバーサイドAPI (/api/url-check, /api/ogp) のSSRF脆弱性と、svelte.config.js でのCSRF保護全面無効化。これらは外部から直接悪用可能。
カテゴリ: セキュリティ
ファイル: src/routes/api/url-check/+server.ts:5-36
説明:
targetUrl パラメータに対してプロトコル検証もドメインホワイトリストも設定されていない。攻撃者は url パラメータに任意のアドレスを渡すことで、サーバーから内部ネットワークやクラウドメタデータサービスへHTTPリクエストを送出させることができる。
GET /api/url-check?url=http://169.254.169.254/latest/meta-data/
GET /api/url-check?url=http://internal-db:5432/
GET /api/url-check?url=file:///etc/passwd
推奨修正:
const ALLOWED_PROTOCOLS = ['https:'];
const parsed = new URL(targetUrl);
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
throw error(400, { message: 'Only HTTPS URLs are allowed' });
}
const blockedRanges = /^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0|169\.254\.|::1|\[::1\])/i;
if (blockedRanges.test(parsed.hostname)) {
throw error(403, { message: 'Private addresses are not allowed' });
}カテゴリ: セキュリティ
ファイル: src/routes/api/ogp/+server.ts:6-46
説明:
unfurl() に渡すURLのバリデーションが不十分。parsedUrl.host の検査は行われているが、プライベートIPアドレス、file:// プロトコル、内部サービスへのアクセスをブロックするチェックがない。unfurl.js はリダイレクトを自動追跡するため、間接SSRF(オープンリダイレクト経由)も成立する。
Nostrユーザーが投稿内に内部URLを含めるだけで、そのURLを閲覧した他のユーザーのブラウザがサーバー経由で内部リソースにアクセスしてしまう。
推奨修正: url-check エンドポイントと同様のプロトコル・IPバリデーションを追加。
カテゴリ: セキュリティ / アーキテクチャ
ファイル: svelte.config.js:22-24
説明:
csrf: { trustedOrigins: ["*"] }全オリジンからのPOSTフォーム送信を許可しており、CSRF攻撃に対して完全に脆弱。コメントに「共有のときにCross-site POST form submissions are forbiddenが出る」とあるが、Web Share Target APIの /post エンドポイントのみを除外すれば十分。
推奨修正:
csrf: {
trustedOrigins: [
'https://lumilumi.app',
'https://lumilumi.vercel.app'
]
}カテゴリ: バグ
ファイル: src/lib/func/reactions.ts:50-52
説明:
// rxNostr3 のリレーエラーを検出しているのに rxNostr に reconnect している
relays.forEach(([key, value]) => {
get(app).rxNostr.reconnect(key); // ← rxNostr3 であるべき
});この関数は rxNostr3 のリレー接続エラーを検出してループしているにもかかわらず、rxNostr(メインのrxNostrインスタンス)にreconnectを呼んでいる。結果として rxNostr3 のリレー接続は永久に回復されず、リアクション・リポストの受信がサイレントに失敗し続ける。
推奨修正:
get(app).rxNostr3.reconnect(key);カテゴリ: バグ
ファイル: src/routes/NostrElements.svelte:53-59
説明:
const data: EventPacket[] | undefined = queryClient?.getQueryData(timelineQuery);
if (data && data.length <= 0) { // ← "0以下" の場合に data[0] を読む
since = data[0].event.created_at; // ← data が空配列なら undefined アクセス → TypeError
} else {
since = ev[0].event.created_at;
}条件が完全に反転している。data.length <= 0(配列が空)のときに data[0] を参照しようとするため、ランタイムエラーが発生する。
推奨修正:
if (data && data.length > 0) {
since = data[0].event.created_at;
} else {
since = ev[0].event.created_at;
}カテゴリ: TypeScript品質
ファイル: src/lib/func/ogp.ts:48-53
説明:
const response = await fetch(...).catch((err) => console.log(err));
const result = (await response?.json().catch((err) => console.log(err))) as Metadata;fetch が失敗すると response は undefined → response?.json() も undefined → result に undefined が Metadata としてキャストされる → その後 result.open_graph 等にアクセスするとランタイムエラー。
推奨修正:
const response = await fetch(`/api/ogp?url=${encodeURIComponent(url)}`);
if (!response.ok) return { title: '', image: '', description: '', favicon: '' };
const result: Metadata = await response.json();カテゴリ: パフォーマンス
ファイル: src/lib/stores/operators.ts:30, 356-370
説明:
// モジュールスコープで宣言されており、セッション中に永遠に蓄積される
const observedEvents = new Set<string>(); // 行356 — 削除されることがない
const memo = new Map<string, Set<string>>(); // 行30 — eventId → relays、削除なしアプリケーションが動作し続けるほど、受信した全イベントIDがこれらのコレクションに蓄積される。数万件規模になりうる。リセット・上限・TTLのいずれも存在しない。
推奨修正: LRU上限付きキャッシュ(最大5000件で古いものを削除)を採用するか、アプリ状態リセット時にクリアする仕組みを追加する。
カテゴリ: セキュリティ
ファイル: src/hooks.server.ts, src/routes/api/
/api/ogp と /api/url-check にレート制限が存在しない。外部ネットワークへのプロキシとして機能するため、DoS攻撃や帯域幅消費攻撃に悪用可能。
推奨修正: hooks.server.ts にインメモリレート制限(IPベース、60req/分等)を追加。
カテゴリ: セキュリティ
ファイル: src/hooks.server.ts, vite.config.ts
vite.config.ts の server.headers は開発サーバー専用のCSPのみ。本番ビルドには以下が欠如:
Content-Security-Policy(本番環境)X-Frame-OptionsX-Content-Type-OptionsStrict-Transport-SecurityReferrer-PolicyPermissions-Policy
app.html の外部スクリプト model-viewer.min.js もCSPなしでロードされている。
推奨修正: hooks.server.ts の resolve() 後にセキュリティヘッダーを一括設定。
カテゴリ: セキュリティ
ファイル: src/lib/func/markdown-it/markdown-it-link.ts:7-17
const linkRegex = /^<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i;
tokenOpen.attrs = [["href", href]]; // プロトコル検証なしNostr投稿内に <a href="javascript:alert(1)">click</a> を記述した場合、Link.svelte (Simple版) では checkSafe() が適用されず JavaScript スキームのリンクが生成される可能性がある。
推奨修正: プロトコルホワイトリスト (https:, http:, mailto:) を追加。
カテゴリ: セキュリティ
ファイル: src/hooks.server.ts:7-10
const allowedOrigins = [
'https://lumilumi.vercel.app/', // ← トレーリングスラッシュ
'https://lumilumi.app/', // ← トレーリングスラッシュ
];RFC 6454によると Origin ヘッダーの値にはパスを含まない(例: https://lumilumi.app)。設定された値は実際のリクエストにマッチしない。
推奨修正: トレーリングスラッシュを削除。
カテゴリ: バグ
ファイル: src/lib/func/zap.ts:183-209
// 取得時
let queryRelay = queryClient.getQueryData(["defaultRelay", pubkey]);
// 保存時(別のキーに保存)
queryClient.setQueryData(["relays", pubkey], ...);取得キーと保存キーが異なるため、キャッシュが永遠にヒットせず毎回リレーにリクエストが発生する。
推奨修正: 保存キーを ["defaultRelay", pubkey] に統一。
カテゴリ: バグ
ファイル: src/lib/stores/operators.ts:96-103
event.tags?.[0]?.[1] && // ← タグ配列の先頭だけを見ている
event.tags[0][1] === identifier // ← d タグが必ず tags[0] にあると仮定NIP-01では d タグが必ずtagsの最初の要素に来る保証はない。他のタグが先頭に置かれているイベントではフィルタが誤って失敗する。
推奨修正:
event.tags.find(tag => tag[0] === "d")?.[1] === identifierカテゴリ: バグ
ファイル: src/lib/stores/operators.ts:271-279
export const zappedPubkey = (event: Nostr.Event): string | undefined => {
return JSON.parse(
event.tags.find((tag) => tag[0] === "description")?.[1] ?? ""
).pubkey; // ← description 内の kind 9734 の pubkey はザップ「送信者」
};NIP-57ではZapレシート (kind 9735) の p タグがザップ対象pubkeyを直接保持している。description内のpubkeyはザップ送信者のpubkey。
NIP-57準拠の修正:
event.tags.find(tag => tag[0] === "p")?.[1]カテゴリ: バグ
ファイル: src/lib/func/nostr.ts:615-651
completeOnTimeout(timeout) と外側の setTimeout(timeout + 10000) が二重に設定されている。error コールバックは clearTimeout を呼んでいるがsubscriptionをアンサブスクライブしていない。
推奨修正: error コールバックでも subscription.unsubscribe() を呼ぶ。
カテゴリ: TypeScript品質 ファイル: 9ファイル
該当箇所:
src/routes/+layout.svelte(L279, L304)src/routes/Menu.svelte(L42)src/routes/[note=note]/+page.svelte(L41)src/routes/Sidebar.svelte(L28)src/lib/func/nostr.ts(L150)src/lib/components/UserPicker.svelte(L28)src/lib/components/NostrElements/UserDataList.svelte(L34)src/lib/components/SettingsElements/Kind30078.svelte(L69)src/lib/components/SettingsElements/Settings.svelte(L98, L112)
テーマ初期化の失敗、localStorage読み書き失敗、JSONパースエラーが本番環境で一切通知されない。
推奨修正: 最低限 console.error(error) を記録。重要な処理ではユーザーフィードバックを追加。
カテゴリ: TypeScript品質
ファイル: src/lib/func/useReq.ts (L34, L41, L136, L141, L244, L249, L351, L356), src/lib/func/nostr.ts (L405)
デバッグ不可能。
推奨修正: throw new Error("具体的なメッセージ") を使用。
カテゴリ: TypeScript品質
ファイル: src/lib/func/reactions.ts:28-34
export function set3Relays(relays: any) {
if (!get(app).rxNostr3) {
get(app).rxNostr3 = createRxNostr(...); // ストアの中のオブジェクトを直接書き換え
}Svelteの writable ストアの内部オブジェクトを app.update() を経由せず直接変更。リアクティビティをバイパスし未定義の挙動を引き起こす。
推奨修正:
app.update((state) => ({ ...state, rxNostr3: createRxNostr(...) }));カテゴリ: バグ
ファイル: src/lib/func/event.ts:129-136, src/lib/func/util.ts:241-256
// event.ts — splice で渡されたオリジナル配列を直接変更
viewEventIds.splice(index, 1);
return viewEventIds;
// util.ts — Array.sort() は in-place ソートで元の配列を変更
return events.sort(...);ストア内部のオブジェクトが直接変更されるため、Svelteのリアクティビティが正しくトリガーされない場合がある。
推奨修正:
// event.ts
const newArr = [...viewEventIds];
newArr.splice(index, 1);
return newArr;
// util.ts
return [...events].sort(...);カテゴリ: TypeScript品質
ファイル: src/lib/func/nostr.ts:407-413
_rxNostr.send(ev).subscribe((packet) => {
// コメントアウトされたデバッグコードのみ
});Subscription を保存せず、送信エラーのハンドラもない。OKパケットのエラー応答 (packet.ok === false) も無視。
推奨修正: error ハンドラを追加し、ok === false のパケットを処理する。
カテゴリ: パフォーマンス
ファイル: src/lib/func/useReq.ts:64-85 (useReq), 163-185 (useForwardReq), 378-400 (useGlobalReq)
obs.subscribe({
next: (v) => { ... },
complete: () => status.set("success"),
error: (e) => { ... },
});
// Subscription オブジェクトを受け取っているが unsubscribe() を呼び出す手段がない特に useForwardReq のObservableはフォワードリクエストのため永続する。コンポーネントアンマウント時にも購読が残り続ける。
推奨修正: onDestroy またはTanStack Queryの queryFn キャンセル機構 (signal.aborted) でサブスクリプションを解除。
カテゴリ: パフォーマンス
ファイル: src/lib/components/Debug/DebugPanel2.svelte:14-16
onMount(async () => {
const eruda = await import("eruda"); // 本番環境でも動的インポートされる
erudaInstance = eruda.default.init();
});import.meta.env.MODE === "development" などの条件チェックなく本番でもインポートされる。
推奨修正: import.meta.env.DEV によるガードを追加。
カテゴリ: パフォーマンス
ファイル: src/lib/func/nostr.ts:122-152
metadataQueue.subscribe((queue) => {
const metadataStr = localStorage.getItem(STORAGE_KEYS.METADATA); // 同期読み取り
let currentMetadata = JSON.parse(metadataStr);
// ...
localStorage?.setItem(STORAGE_KEYS.METADATA, JSON.stringify(currentMetadata));
});フォロイーが数百人いる場合、数MBのJSON書き込みが頻繁に発生。JSON.stringify(key) === JSON.stringify(key) による検索もO(n)。
推奨修正: IndexedDBの使用、またはpubkeyごとに個別キーに分割。
カテゴリ: アーキテクチャ
ファイル: src/lib/func/nostr.ts
RxNostr初期化、リレー管理、メタデータlocalStorage永続化、イベント公開、ページネーション付きリクエスト、メディアリクエストなど、極めて多くの責務を1ファイルに詰め込んでいる。50ファイルからimportされている。
推奨修正: relay-manager.ts, metadata-cache.ts, event-publisher.ts, req-helpers.ts 等に分割。
カテゴリ: アーキテクチャ
ファイル: src/lib/func/*.test.ts
テストファイルは8つのみ(event.test.ts, contentCheck.test.ts, content.test.ts, makezap.test.ts, markdonw.test.ts, searchQueryParser.test.ts, sortTest.test.ts, upload.test.ts)。コンポーネントテスト、統合テスト、E2Eテストは存在しない。
推奨修正: 最優先で operators.ts, muteCheck.ts にテストを追加。@testing-library/svelte でコンポーネントテストも実施。
カテゴリ: セキュリティ
ファイル: src/lib/components/NostrElements/kindEvents/Status/GeneralStatusDisplay.svelte:28-36, OtherStatusDisplay.svelte:25-37
<a href={link}> <!-- checkSafe() なし -->Link.svelte コンポーネント (Elements/Link.svelte) を経由しないため checkSafe() のバリデーションが適用されない。
推奨修正: Elements/Link.svelte を使用するか、同等のURL検証を適用。
カテゴリ: セキュリティ
ファイル: src/app.html:15-18
<script
type="module"
src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js"
></script>CDNが侵害された場合、任意のスクリプトが実行される。
推奨修正: integrity="sha384-..." と crossorigin="anonymous" を追加。
カテゴリ: バグ
ファイル: src/lib/func/nip96.ts:219-221
NIP-96では delegated_to_url 指定時に api_url が空でなければならないが、api_url が空文字列(falsy)の場合に先のチェックで既に false を返してしまう。
カテゴリ: バグ
ファイル: src/lib/func/nostr.ts:909-912
until = lastEvent.event.created_at; // 同じ created_at のイベントが次ページで重複推奨修正: until = lastEvent.event.created_at - 1;
カテゴリ: バグ
ファイル: src/lib/stores/operators.ts:110-117
({ event }) => `${event.kind}:${event.pubkey}:${event.tags[0][1]}`
// tags が空配列の場合 TypeError推奨修正: event.tags.find(t => t[0] === "d")?.[1] ?? "" を使用。
カテゴリ: バグ
ファイル: src/lib/func/mute.ts:22-26
>= を使っているため、同じ created_at(同一イベント)でも上書き処理が走る。> が意図に沿う。
カテゴリ: TypeScript品質
ファイル: src/lib/func/nostr.ts:490, src/lib/func/publishError.ts:13
} catch (error: any) {
if (error.message.includes(...)) // error が string や null の場合にランタイムエラー推奨修正: error instanceof Error でガード。
カテゴリ: TypeScript品質 ファイル: 複数
Profileの[key: string]: any—src/lib/types.ts:117set3Relays(relays: any)—src/lib/func/reactions.ts:27filters: any—src/lib/func/nostr.ts:711-713MenuItem.icon: any—src/lib/types.ts:22FilterConfig[key: string]: any—src/lib/stores/ChunkManager.ts:18
推奨修正: 具体的な型定義に置き換え。
カテゴリ: TypeScript品質
ファイル: src/lib/types.ts:28, src/lib/func/nostr.ts:937
export type ReqStatus = "loading" | "success" | "error";
// 使用箇所:
status.set("nodata" as ReqStatus); // 型に含まれていない値をキャスト推奨修正: "nodata" を ReqStatus 型に追加。
カテゴリ: パフォーマンス
ファイル: src/lib/stores/operators.ts:148-162
const sorted = sortEventPackets([...acc, a]); // O(n log n) per event500件蓄積後、毎回新しいイベントで500件のコピーとソートが走る。
推奨修正: insertEventPacketIntoDescendingList の問題を再調査し、binary insert (O(log n)) に変更。
カテゴリ: パフォーマンス
ファイル: src/lib/func/muteCheck.ts:18-56
ホットパス(全イベントに対して呼出)で get() が重複実行される。Svelte storeの get() は毎回一時サブスクリプションを作成する。
推奨修正: 先頭で1度だけ取得して引数として渡す。
カテゴリ: パフォーマンス
ファイル: src/lib/components/NostrElements/content/Content.svelte:47-71
ダイアログが閉じていても ContentParts が2つ存在する。テキスト解析、URL型チェック、OGPロードが2倍のコスト。
推奨修正: ダイアログ用の ContentParts を {#if dialogOpen} で条件付きレンダリング。
カテゴリ: パフォーマンス
ファイル: src/lib/func/ogp.ts:27-42
キャッシュ未命中時に表示中の全URLに対して同時にフェッチが発生。50投稿×2URL = 100件の同時リクエスト。
推奨修正: 同時実行数を制限するキュー(例: p-limitで5並列に制限)。
カテゴリ: アーキテクチャ
ファイル: src/lib/stores/stores.ts, src/lib/stores/globalRunes.svelte.ts
データアクセスが $store, store.get(), .value の3種類に分散し、認知負荷が高い。
推奨修正: 段階的にSvelte 5の $state ベースに統一。
カテゴリ: アーキテクチャ
ファイル: src/routes/[npub=npub]/+page.svelte
同じ Metadata+EventCard パターンが5回以上コピーされている:
<Metadata queryKey={["metadata", event.pubkey]} pubkey={event.pubkey}>
{#snippet loading()}<div><EventCard note={event} /></div>{/snippet}
{#snippet nodata()}<div><EventCard note={event} /></div>{/snippet}
{#snippet error()}<div><EventCard note={event} /></div>{/snippet}
{#snippet content({ metadata })}<EventCard {metadata} note={event} />{/snippet}
</Metadata>推奨修正: MetadataEventCard 統合コンポーネントを作成。
カテゴリ: アーキテクチャ
ファイル: src/lib/components/NostrElements/kindEvents/EventCard/EventCard.svelte
31個のKindコンポーネントが静的にimportされている。
推奨修正: kindからコンポーネントへのMapレジストリパターン + 頻度の低いkindは動的import。
カテゴリ: アーキテクチャ
ファイル: src/routes/
SvelteKitのデフォルトエラーページが表示される。Nostrクライアントではリレー通信エラーが頻繁に発生するため、グレースフルなエラー表示は重要。
カテゴリ: アーキテクチャ
ファイル: src/lib/func/util.ts:154-171, src/lib/func/nostr.ts:491-498
"イベントデータのサイズが無効"
"署名がキャンセルされました"推奨修正: i18nキーに置き換え。
カテゴリ: アーキテクチャ
ファイル: src/lib/stores/operators.ts
metadata(), bookmark(), userStatus(), saveEachNote(), reactionCheck() が tap/filter 内でストア書き込み、localStorage更新、キャッシュ更新を行う。
推奨修正: 副作用は subscribe 側で実行。
カテゴリ: パフォーマンス
ファイル: src/lib/func/nostr.ts:191-225
フォロイー500人で各メタデータ更新時に最大500回の JSON.stringify ペア比較。
推奨修正: Map<string, [QueryKey, EventPacket]> でO(1)ルックアップ。
カテゴリ: パフォーマンス
ファイル: src/lib/components/renderSnippets/nostr/MainTimeline.svelte:136-138
let result = $derived(useMainTimeline(queryKey, configureOperators(), filters));$derived の再計算時に新しい createQuery インスタンスが生成される可能性。
ファイル: src/lib/func/constants.ts:33
export const monoZap = {
endoiunt: "https://...", // "endpoint" のタイポファイル: 84ファイル
settings.ts (21件), nostr.ts (18件), NostrMain.svelte (18件) が特に多い。リレーURL、フィルター条件、内部処理状態が出力される。
推奨修正: ビルド時に除去するか、import.meta.env.DEV でガード。
ファイル: src/lib/func/nip05.ts:66-69
"Failed to fetch" はネットワーク断絶、DNS解決失敗などCORS以外でも発生する。
ファイル: src/lib/func/nip96.ts:263
} catch (_) {
throw new Error(`Error fetching.`);
}元のエラー情報(URL、ステータスコード)が失われる。
ファイル: src/lib/func/upload.ts:487-530
エラーオブジェクトに nip94_event フィールドが欠けているため型定義を満たしていない。
ファイル: src/lib/func/markdonw.test.ts
ファイル: package.json
Svelte 4時代の @melt-ui/svelte とSvelte 5対応版の melt が共存。移行途中。
ファイル: src/routes/+layout.svelte:117-125
返り値のunsubscribe関数が保存・呼び出しされていない。ルートレイアウトなので実際的問題は小さい。
ファイル: src/lib/stores/globalRunes.svelte.ts:41-63
$state.raw ベースで独自subscriptionメカニズムを持つ。Svelteのリアクティビティと二重通知のリスク。
ファイル: src/lib/func/zap.ts:88-134
lud16: "attacker@127.0.0.1" のような値でローカルホストへのリクエストが発生。クライアントサイドなので直接的サーバー影響はないが、内部ネットワークスキャンに悪用される可能性。
ファイル: src/routes/[note=note]/+page.server.ts:130
Svelteのデフォルトエスケープが適用されるため直接XSSにはならないが、</meta><script> のような文字列が含まれる可能性を考慮。
- SSRF修正 —
url-checkとogpにプロトコル制限 + プライベートIPブロック - CSRF修正 —
trustedOrigins: ["*"]を具体的オリジンに変更 - rxNostr3 reconnectバグ — 1行修正
- NostrElements 条件反転 —
<= 0→> 0 - fetchOgpContent のnullチェック追加
- セキュリティヘッダー追加 (
hooks.server.ts) allowedOriginsのトレーリングスラッシュ修正markdown-it-link.tsにプロトコル検証追加- 空catchブロックにログ追加
erudaの本番除外observedEvents/tieMapにLRU上限- キャッシュキー不一致修正 (
zap.ts)
nostr.tsの God Module 分割- 配列ミューテーション → イミュータブルパターン
- テスト拡充 (operators, muteCheck)
- Metadata+EventCard 共通コンポーネント抽出
- LocalStorage → IndexedDB (メタデータ)
- Observable サブスクリプションのクリーンアップ
- Svelte 4 store → Svelte 5 $state 統一
- EventCard レジストリパターン化
+error.svelteエラーバウンダリ追加- i18nハードコード文字列の置き換え
Generated by parallel static analysis with 5 specialized audit agents