Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】The Secure Code Review Challenge — Solution #6: 📥 FileDrop (Username Is User Input Too)

2026年10月02日に「Dev.to」が公開したITニュース「The Secure Code Review Challenge — Solution #6: 📥 FileDrop (Username Is User Input Too)」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

ファイル保存アプリFileDropで、ユーザー名がファイルパスに利用され、検証不足のため、匿名ユーザーが他人のファイルを読み書きできる脆弱性が判明した。ユーザー名に「x/../casey」のような入力で、他のユーザーのファイル領域へ不正アクセスが可能だった。ユーザー入力の厳格な検証や、パス構築にサーバー側IDの利用が重要だ。

ITニュース解説

FileDropは、ユーザーがログインしてファイルをアップロード、リスト表示、ダウンロード、削除できる個人向けファイルストレージサービスだ。このアプリケーションは、Node.jsのExpressフレームワークをバックエンドに、Reactをフロントエンドに用い、MongoDBをデータベースとして使用している。アップロードされたファイルはサーバーのファイルシステムに保存される仕組みだ。ユーザー認証にはJWT(JSON Web Token)を使用し、そのREADMEでは「各アカウントはディスク上に独自のストレージ領域を持ち、その中のファイルはそのアカウント専用でプライベートである」とセキュリティ上の約束を掲げていた。

このFileDropは、一見すると非常に堅牢なセキュリティ対策が施されているように見えた。パスワードの保存には、安全な暗号化技術であるbcryptが使われ、JWTはアルゴリズムを固定して発行されていた。また、NoSQLインジェクション攻撃を防ぐための入力サニタイズ(無害化)も導入され、Webサイトのセキュリティポリシーを定義する厳格なCSP(Content Security Policy)も設定されていた。さらに、ファイルパスからファイル名だけを安全に抽出するpath.basename()関数を使って、ディレクトリを不正に移動するパス・トラバーサル攻撃を防ぐための対策も講じられていた。これらは、まさに「よく設計された」アプリケーションに見える特徴であった。

しかし、これだけの対策にもかかわらず、このFileDropには重大な脆弱性が存在した。それは、匿名ユーザーであっても、他のすべてのユーザーのファイルを読み込み、上書き、削除できてしまうというものだ。この脆弱性の入り口となったのは、ユーザー登録フォームの「ユーザー名」フィールドだった。

セキュアなアプリケーションを開発する際や、既存のアプリケーションのセキュリティレビューを行う際には、いくつかの重要なステップを踏む。まず、アプリケーションの全体像や主要な機能、ユーザーがどのように使うかを理解する。次に、ユーザーからの入力がどこから入ってくるか(エントリーポイント)を特定する。その上で、その入力がデータベースへの問い合わせ、ファイルパスの生成、HTMLの表示といった、アプリケーションの動作に影響を与える可能性がある危険な処理(シンク)を洗い出す。これらの情報をもとに、認証や認可に関する問題、そしてユーザー入力が危険な処理に渡される際のリスク(インジェクション攻撃など)を考慮した脅威モデルを構築する。そして、実装されているセキュリティ対策がどの脅威を実際に防げているのか、逆にどの脅威が防がれていないのかを詳細にレビューする。もし防御されていない脅威があれば、実際にそれを悪用して脆弱性が存在することを証明し、最終的には根本的な修正と、複数の防御策を組み合わせる多層防御の対策を適用する。

このFileDropのレビューにおいて、まさにこの手法が適用された。特に注目されたのは、アプリケーションのファイル操作で使われるパスがどのように構築されるかという点だった。ファイルは常に「STORAGE_ROOT / <ユーザー名> / <ファイル名>」という構造で保存されることが明らかになった。ここで非常に重要なのは、データベースにはファイルの所有権情報が直接記録されておらず、「あなたのファイル」とは「あなたのユーザー名のフォルダにあるファイル」を意味している点だ。つまり、アカウント間のファイル隔離は、req.user.usernameという値が、常に単一で安全なフォルダ名であるという前提に完全に依存していた。

ファイルパスを決定する上で、ユーザー入力が関係する箇所は2つあった。1つはファイル名、もう1つはユーザー名だ。ファイル名については、前述のpath.basename()が適用されるため、../../etc/passwdのような不正なパス・トラバーサル文字列は、passwdというファイル名のみに変換され、ディレクトリを抜け出すことはできない。また、ダウンロードや削除の際にも、.や..といった特殊なファイル名が拒否されるため、この部分は安全に保護されていた。

しかし、もう一方の「ユーザー名」については、状況が異なっていた。ユーザー名は、認証トークンであるJWTから取得され、さらにその元はデータベースから取得されるため、一見すると安全に見えるかもしれない。しかし、このユーザー名は、ユーザーがサインアップ(登録)時に自分で入力したものだった。そして、ユーザー登録時のバリデーション(入力値の検査)は、ユーザー名の「データ型」と「長さ」しかチェックしておらず、スラッシュ(/)やドット(.)などの特殊文字が含まれていても許容されていたのだ。さらに、データベースはユーザー名の前後の空白をトリム(削除)するだけで、その他の詳細な検証は行っていなかった。

ここで問題となるのは、Node.jsのpath.join関数が..(親ディレクトリを意味する)というセグメントを正規化するという挙動だ。例えば、path.join('/app/data/files', 'x/../casey')という記述は、最終的に/app/data/files/caseyというパスに解決される。この特性を利用すると、ユーザー名として「x/../casey」と登録したアカウントは、MongoDB上では独自のユーザーとして扱われるが、ファイルシステム上では既存のユーザー「casey」のフォルダを指すことになってしまう。x/の部分は、..がキャンセルする基準点を提供し、パスが想定されたSTORAGE_ROOT内に留まりつつ、目的のcaseyフォルダに到達できるようにしている。

このような「x/../casey」というユーザー名で登録し、そのアカウントでログインすると、あたかも自分のファイルであるかのようにCaseyのファイルがリスト表示され、ダウンロード可能になる。例えば、Caseyの機密ファイルである「private-keys-backup.txt」をダウンロードすることも容易にできてしまう。この攻撃手法は、ファイルのアップロードや削除にも適用可能であり、さらには「a/../../../..」のようなユーザー名を登録することで、Node.jsプロセスがアクセス可能なファイルシステム上の任意の場所にアクセスできるようになる。

この脆弱性の影響は極めて深刻だ。全く新しい匿名アカウントでも、被害者からのいかなる操作もなしに、他のユーザーのすべてのファイルを読み取り、上書き、削除できるため、ファイルが持つ機密性、完全性、可用性のすべてが失われる。これは、不正なパス指定により本来アクセスできないディレクトリ内のファイルにアクセスするCWE-22(パス・トラバーサル)およびCWE-73(ファイル名またはパスの外部制御)に分類される脆弱性であり、結果として見かけはIDOR(不適切な直接オブジェクト参照)のように見えるが、その根本原因は、ファイルパスを構築する際にユーザー入力が不適切に扱われたことにあった。

この脆弱性を修正するには、いくつかの選択肢がある。最も推奨される方法は、ファイルパスをユーザー名のようなユーザー入力から構築することを完全にやめることだ。代わりに、MongoDBのObjectIdのような、サーバーが生成する不変で一意のユーザーIDをファイルパスに使うべきである。これならば、攻撃者が選択したテキストがパスに到達することは決してない。

もしユーザー名をパスの一部として使う必要がある場合は、ユーザー登録時にユーザー名に許可される文字を厳格なホワイトリスト方式で制限すべきだ。例えば、英数字、アンダースコア、ハイフンのみを許可し、スラッシュやドットは禁止する正規表現を適用する。さらに、ファイルパスを構築した後、最終的なパスが必ず意図したベースフォルダ(STORAGE_ROOT)内に収まっていることをプログラム的にチェックする防御策も不可欠だ。加えて、ファイル所有権をデータベースに明示的に記録し、ダウンロードや削除の際にその所有権をチェックするよう改めることも重要である。path.basename()によるファイル名のサニタイズや、空のファイル名、.、..の拒否は引き続き行うべき対策である。

この事例から得られる重要な教訓は、入力の出所がデータベースやJWTのような信頼できる場所であっても、その元がユーザー入力であれば、それは決して信頼してはならないということだ。ユーザー名のような一見無害に見える入力も、それがファイルパスのような機密性の高い「シンク」(処理先)に到達する場合、厳格な検証と防御策が必要となる。アプリケーションは、コードの目に見える部分だけでなく、すべてのユーザー入力をたどり、その最終的な影響を評価する視点を持つことが極めて重要だ。FileDropはパスの明白な半分(ファイル名)を保護したが、もう一方の半分(ユーザー名)をオープンにしたままだった。ユーザー名は単なる表示ラベルではなく、ここではファイルシステム上のフォルダ名として機能していたのだ。

関連コンテンツ

関連IT用語