2 ポイント 投稿者 GN⁺ 2024-06-02 | 1件のコメント | WhatsAppで共有
  • AWSのデータ漏えいリスクでは、S3バケットへの不正アクセスが繰り返し登場し、古いAPI設計と例外的な挙動のため、単純に「公開/非公開」と判断しにくい
  • 多くのS3操作は一般的なAWSエンドポイントではなく、バケットURLそのものに対して呼び出され、誤ったバケットポリシーでは認証なしのcurlリクエストでも危険な操作が可能になる
  • s3:ListBucketを防いだだけで安全とは言い切れず、ListBucketVersionsListMultipartUploadsfetch-ownerのような経路からオブジェクトキーとアカウント識別子が露出しうる
  • アップローダーはストレージクラス、タグ、Object Lock、一部のリダイレクト関連ヘッダーなどオブジェクト属性に影響できるため、IAM条件やライフサイクルポリシーのような追加制御が必要になる
  • ACLとパブリックアクセスブロック設定だけを見て非公開と判断すると見落とす経路があり、CloudFrontディストリビューションやCognito identity pool経由でインターネット利用者がS3オブジェクトにアクセスできる

古いS3 API設計と匿名呼び出し

  • S3はAWS初期のサービスの1つであり、堅牢で十分にテストされているが、標準化された設計パターン以前の名残のため、他のAWSサービスとは異なるAPIの形を持つ
  • 一部のS3 APIは s3.us-east-2.amazonaws.comのような一般エンドポイントを使うが、多くの操作は対象のバケットURLに直接リクエストする必要がある
    • バケット一覧取得の例はHost: [bucketname].s3.amazonaws.com形式のGET /
    • バケットタグ取得もそのバケットホストにGET /?taggingを送る
  • EC2やDynamoDBのような多くのAWSサービスは一般エンドポイントを使い、対象リソースをHTTPヘッダーやパラメータで渡す方式が一般的
  • S3バケットはパブリックアクセスと認証付きアクセスの両方をサポートするため、どのAPI操作が認証なしで可能かは常に明確とは限らない
  • 例のバケットポリシーがPrincipal: "*"Action: "s3:*"をバケットリソースに許可していると、認証なしリクエストでもバケット削除が可能になる
  • 一部の操作は匿名リクエストをサポートせず、s3:GetBucketOwnershipControls does not support Anonymous requests!のようなエラーが返る
  • 匿名APIリクエストはCloudTrailにanonymousアカウントとして記録される
    • 認証されていないリクエストだと、誰がバケットを削除したのか、暗号化設定やロギング状態を確認したのかを特定できない
  • /?logging/?tagging/?encryptionのようなパスはブラウザでもテストできる
  • GetObjectTorrentのように、ドキュメントには残っているがもはや実行できない操作もある

ListBucketを防ぐだけではオブジェクトキー露出を防ぎにくい

  • S3オブジェクトをダウンロードするには各オブジェクトのキーが必要で、キーはファイルパスのように使われる
  • ルートバケットへのGETリクエストは条件次第でバケット内容を返すため、s3:ListBucket拒否がよくある防御策のように見える
  • public-read ACLとs3:ListBucket拒否ポリシーを併用しても、オブジェクトキーを取得する経路は残る
    • GET /?versions、つまりs3:ListBucketVersionsは、バケット内オブジェクトバージョンのメタデータを返す
    • GET /?uploads、つまりs3:ListMultipartUploadsは、進行中のmultipart upload一覧を返す
  • HeadBucketのドキュメントにはバケットの存在有無とアクセス権確認に関する記述があるが、実際にはListBucket操作の実行権限があるかを確認している
  • ListBucket拒否だけを検証すると、S3オブジェクトキー露出の可能性を見落としうる

未完了のmultipart uploadのコストと露出

  • multipart uploadはcreate-multipart-uploadで開始し、upload-partでパートをアップロードする
  • 未完了のmultipart uploadはWebコンソールでは確認しにくく、/?uploadsまたはaws s3api list-multipart-uploads --bucket [bucket-name]で確認できる
  • 完了リクエストが正常に送信されなかった場合、Amazon S3はパートを組み立てず、オブジェクトも作成しない
    • アップロード済みのパートはmultipart uploadが完了または中断されるまでアカウントに残る
    • 保存されたパートにはS3ストレージ料金が発生する
  • 完了前のオブジェクトのパートをダウンロードする方法は見つからなかったが、削除は可能
  • AWSは、一定日数後に未完了アップロードを削除するライフサイクルルールの適用を推奨している
  • /?uploadsで未完了のmultipart uploadを列挙すると、アップロードを開始したprincipalのARNが返される
    • アカウントIDやARNのような識別子を機微ではないとみなす立場なら問題ではないかもしれない
    • 攻撃者に有用な識別子の公開を避けたいなら、露出と見なせる

ACLとメールアドレスベースのアカウント確認

  • S3 ACLのドキュメントには、AWSアカウントをルートユーザーのメールアドレスで識別していた時代の名残がある
  • PutBucketACL操作ではgranteeをメールアドレスで指定できる
    • Type: AmazonCustomerByEmailEmailAddressを使う
  • 指定したメールアドレスに紐づくAWSアカウントがなければUnresolvableGrantByEmailAddressエラーが発生する
    • エラーメッセージは「提供されたメールアドレスは記録上どのアカウントにも一致しない」という形になる
  • この挙動により、特定のメールアドレスが登録済みのAWSアカウントを持っているか確認できる

アップローダーが選べるストレージクラスとオブジェクトメタデータ

  • S3のストレージクラスはバケットではなくオブジェクトに適用される
  • バケット単位で希望するストレージクラスを固定する設定はなく、アップロード主体がオブジェクトのストレージクラスを指定できる
    • 例はaws s3 cp "my.txt" "s3://mybucket/myobject.txt" --storage-class [CLASS]
  • アップローダーは、あらかじめ定義された一覧の中で、バケット所有者が負担するGBあたりの保存・アクセスコストに影響を与えられる
  • IAMポリシーでs3:x-amz-storage-class条件キーを使えば、許可するストレージクラスを制限できる
    • 例のポリシーはs3:PutObjectに対してSTANDARDのみ許可する
  • ライフサイクルポリシーを設定すれば、一定期間後にすべてのオブジェクトを特定のストレージクラスへ移行できる
  • pre-signed URLを使うアップロードでは、AWS Signature Version 4がX-Amz-で始まるすべてのヘッダーの署名を要求する
    • ストレージクラスはx-amz-storage-classヘッダーで指定される
    • アプリケーションがよほどひどく実装されていない限り、即座に改ざんする明確な方法はない

タグ、Object Lock、リダイレクトもアップローダーの影響下にある

  • S3オブジェクトに関する多くの属性はアップローダーが制御する
  • オブジェクトタグはアップロード時に指定できる
    • 例は--tagging "AllYourTags=AreBelong&To=Us"
  • タグ値を基準に自動化を行うシステムは、アップローダーが作ったタグ値の影響を受けうる
  • Object Lockは、バケットでobject lockingが有効な場合、オブジェクト保持やlegal holdを設定できる
    • 例のコマンドは--object-lock-retain-until-date "2099-01-01T00:00:00+0000"--object-lock-legal-hold-status "ON"--object-lock-mode "COMPLIANCE"を使う
  • 静的Webサイトホスティングが有効なバケットでは、アップロードされたファイル設定を使ったオープンリダイレクトが可能
  • PutObjectがサポートする全ヘッダー一覧にも注意が必要
    • pre-signed URLには制限がある
    • Cognito identityとIAMポリシーに依存する構成では、認証済みCognitoコンテキストでリクエストに署名できる

バケット所有者とアカウント識別子の露出

  • 特定のアカウントIDがアクセス可能なバケットの所有者か確認するには、ListBucketリクエストにx-amz-expected-bucket-ownerヘッダーを入れられる
    • 間違ったアカウントIDを入れるとAccessDeniedが返る
    • 正しいアカウントIDを入れ、呼び出し元にListBucket権限があれば正常応答が返る
  • ListBucket APIのfetch-owner=trueパラメータを使うと、応答の各キーにOwner要素が含まれる
  • Owner内のIDは、AWSドキュメントでcanonical user IDと呼ばれる64文字の16進文字列
    • AWSアカウントIDを難読化した形である
  • canonical user IDをIAMポリシーのPrincipalCanonicalUserとして入れて保存し、再読み込みするとAWSアカウントIDとして解釈される
  • ListBucketVersionsListMultipartUploadsfetch-ownerなしで同様に動作する

S3オブジェクトキーはファイル名のようでいて違う動作をする

  • S3のオブジェクトキーは大文字小文字を区別する
  • 同じ名前に見えても大文字小文字が違えば複数のオブジェクトをアップロードできる
  • アプリケーションがS3オブジェクトキーを大文字小文字を区別しないファイル名のように扱うと問題が起きうる
    • 例のアプリケーションはユーザーのパスワードをS3ファイルに保存し、ファイル名としてユーザー名を使う
    • サインアップ時にはファイルの存在有無だけを確認し、パスワード変更時にはユーザー名を小文字化してファイルに書き込む
    • jeffがすでに存在していてもJEFFとして登録でき、JEFFユーザーがパスワード変更でjeffのファイルを上書きできる
  • S3オブジェクトキーにはあらゆるUTF-8文字を使える
    • 特定の文字は一部のアプリケーションやプロトコルで問題を引き起こすことがある
    • 空白、スラッシュ、パーセント文字などもオブジェクトキーとして有効

「非公開バケット」に見えてもアクセス経路が残ることがある

  • ACLが無効で、リソースポリシーが狭く設定され、block public accessが有効でも、バケットが公開アクセス可能なことがある
  • 最も一般的な経路はAmazon CloudFrontディストリビューション
    • S3バケットの前段にCDNを置く場合、通常はインターネット向け配信を意図している
    • セキュリティツールは、バケットのリソースポリシーがCloudFrontに限定されていると公開ではないと判断しうる
  • 例ではget-bucket-policy-statusIsPublic: falseを返す
    • バケットへ直接リクエストするとAccessDeniedが返る
    • CloudFrontディストリビューションのドメインへ同じリクエストを送るとオブジェクト内容が返る
  • Cognito identity poolも、制限されたリソースポリシーを持つバケットを露出させうる
    • Cognitoはログイン成功後、事前設定されたロールの一時的なAWS認証情報を提供する
    • そのロールにs3:ListBuckets3:GetObject権限があれば、利用者はS3 APIを呼び出せる
  • 公開アクセスに該当しうるCognito設定は2つある
    • Self-registration: インターネット利用者がアプリに登録してログインできるなら、事実上公開アクセスになる
    • Guest access: 認証していない利用者に一意の識別子とAWS認証情報を提供する
  • ゲストアクセスの例は、get-idIdentityIdを取得し、get-credentials-for-identityで一時認証情報を受け取り、そのプロファイルでaws s3 lsを実行する流れ
  • CloudFrontとCognito identity poolは実際にインターネットでよく使われているが、セキュリティツールではめったに表示されない公開アクセス経路である

1件のコメント

 
GN⁺ 2024-06-02
Hacker News の意見
  • 興味深い点は多いが、ファイルシステムが大文字小文字を区別することを不満点と見るのには同意しにくい
    本来そうあるべきだと思うし、macOS がそうしていない点のほうがむしろ苛立たしい

    • 「本来そうあるべき」というのがなぜなのか分からない。Windows も大文字小文字を区別しないので、S3 がほぼ普遍的な慣例に反しているわけでもない
      ファイル名の大文字小文字の区別は、非技術系ユーザーにとっても意外に感じられることがある。誰かが「Book Draft 1.docx」を送ったと言ったのに、メールボックスには「Book draft 1.docx」があったとして、普通は「別のファイルを送ったようですね?」とは言わない
      文章でも大文字小文字はたいてい意味を変えない。「Hi, how are you?」と「hi, how are you?」は同じ意味で、大文字が意味を変えるのは固有名詞と普通名詞を区別する場合くらいだが、ファイル名ではめったに気にされない
    • 技術的な実装の観点では、「A」と「a」は別の文字だという点は ASCII や Unicode などでよく確立されている
      個人の好みは別として、開発者やシステム管理者がファイルシステムの大文字小文字の区別に驚いたり苛立ったりするのは理解しにくい。必要なら、検索結果のようにエンドユーザー向けには開発者が抽象化してあげればよい
    • 筆者です。不満というより観察に近い。絶対的に良い/悪いの問題ではなく、アプリケーション設計時に考慮すべき要素だ
    • ファイル名が大文字小文字を区別することで得られる利点が正確に何なのか分からない。逆であれば、そもそも起こり得ないよくあるミスを大量に招く
      コーディングのようにコードスタイルを強制して可読性に役立つわけでもない。プログラミングでも、IDE が変数名のタイプミスを見つけられるほど賢くなるまでは、バグの原因になりがちだった。Pascal の良かった点の一つは、C と違って大文字小文字を気にしなくてよいことだった
    • macOS は大文字小文字の保持はする。個人的には、両方の利点をうまく組み合わせた方式だ
      ファイル名を望むスタイルで書けて、その表記は保持されるが、検索したり処理したりするときには、そのスタイルを正確に覚えていなくてもよい。検索は大文字小文字を区別しないからだ
  • 大文字小文字の区別はまだ分かりやすいほうで、もっと直感的でないのはS3 のパスが見せかけだという点だ
    S3 は「/builds/1/installer.exe」のアップロードを受け付け、/builds 配下の一覧も表示するが、実際には名前に「/」を含む「/builds/1/installer.exe」というキーを 1 つアップロードしているだけだ
    そのため「/builds/1//installer.exe」や「/builds//1/installer.exe」もアップロード可能で、完全に別のファイルになる。単なるキー名であって、実際のディレクトリは存在しない

    • その通り。ただし新しい S3 Directory buckets [1] を使うと例外で、これがむしろ全体をさらに分かりにくくしている
      [1] https://docs.aws.amazon.com/AmazonS3/latest/userguide/direct...
    • 「/」はデフォルトのパス区切り文字にすぎない、という点も見落としてはいけない。ファイル名に「/」が必要なら、好きな別の文字を区切り文字として使える: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObje...
    • パスを何らかの標準形として解釈すること、たとえば重複した / をまとめること以外に、実際のディレクトリが接頭辞と本質的に何が違うのか分からない
    • 接頭辞方式は本当に多くのバグを生む。AWS がなぜそうしたのかは理解できるし、実際に賢いアプローチではあるが、それでも多くの開発者が引っかかる
      今年、うちの本番システムも奇妙なバグに遭遇し、5人がかりでようやく見つけた。原因は名前が文字通り「/」のオブジェクトで、ソフトウェアがそれをファイルではなくパスのように扱おうとしていたことだった
  • S3 や他の AWS サービスを安心して使うのは難しい。直感的なものがなく、動く要素が多すぎ、読むべきドキュメントも多すぎる
    それでも原文のように、うっかりすべてを世界中に公開してしまうことがあり得る。むしろ Hetzner Storage Boxes や DigitalOcean Spaces のような、本当に単純なサービスを使いたい

    • DigitalOcean Spaces は好きだが、そこにも厄介な癖がある
      最近、数 MB より大きい動画ファイルをパイプでアップロードすると、返された Location から https:// が抜けることに気づいた。だからファイルをアップロードするたびに Location が https で始まっているか確認し、なければ付け足さなければならない
      当然ながら S3 Node クライアントの GitHub issue では「DigitalOcean のバグのようですね」と言われ、DigitalOcean フォーラムでは「S3 Node クライアントのバグのようですね」と言われる
    • DigitalOcean のシークレット値の扱い方は、誰でも怖くなるようなものだ。Container Registry を使い、K8S が自動アクセスするよう設定すると、そのサービスが Spaces にフルアクセス可能な secret を作ることを知っていただろうか?
    • 数年間クラウド開発から離れて主にクライアント側の仕事をしていて、最近戻ってきたのだが、その間に積み上がった複雑さと、パブリッククラウドで鉄壁の解を作るために必要な認知負荷に驚いた
      もともとは一部の特殊な状況を助けるために設計された多数の機能や癖が、今では一般的なプロトコルの一部になっている。ビジネス上、誰も背を向けないようにしようとした結果、そうなったように思える
  • 数十億個のオブジェクトを削除する場合も注意が必要だ。削除 API を直接呼ぶと高くつくことがある。
    代わりに、ワイルドカードやバケット全体に対して有効期限を now に設定するライフサイクルルールを無料で設定できる。そうすればストレージ課金は即座に止まり、削除は AWS が自動で処理してくれる。

    • 厳密に言えば削除呼び出しは無料で、費用がかかるのはオブジェクトを取得するための一覧取得呼び出しだ。理論上、別のソースからどのオブジェクトがあるか分かっていれば無料で済む。
    • ライフサイクルルールの効果は即時ではない。1 日 1 回走るバッチ処理で適用されるため、削除がすぐ起きるわけではない。
    • AWS が実際の削除タイミングを選べるためだ。メタデータ上ではオブジェクトが削除済みと表示し、AWS は利用量の少ない時間帯に削除を処理できる。
      S3 API サーバーが秒間リクエスト数で叩かれる事態も避けられる。
  • 失敗したマルチパートアップロードが見えない形で残り、明示的にライフサイクル設定をしないとストレージ費用まで発生するという点は本当にひどい。
    「Simple」の S は単純さを意味するものだと思っていた。

    • その通り、それはひどい。当時 S3 の GM だった ahenry@ を責めればいい。
      私の提案は、未完了アップロードのパートは最後のアクティビティから 24 時間だけ残し、その間のストレージ料金も請求しない、というものだった。ahenry@ はそれを却下した。
    • この問題で私たちは数千ドルを失った。
      かなり古いサーバーで、ほぼ 10 年間、毎晩マルチパートアップロードを開始する cron スクリプトが動いていた。バックアップをバケットに押し込むためのものだったが、そのバケットはユーザーのアップロードコンテンツも保存していたので、毎日少しずつ大きくなるのは正常に見えた。
      スクリプトは「動いていない」状態だったためバックアップデータには依存しておらず、S3 上にもファイルは見えず、バケットサイズは着実に、しかし過度ではない程度に増えていた。ところがこの春に確認したところ、ほぼ 3TB の未完了マルチパートアップロードを保存していた。
      もちろん、この逸話が悪い慣行だらけなのは分かっている。
    • その S は、コストを跳ね上げるシンプルな方法の S だ。
    • 「Simple」という名前は、代替手段がディスク付きサーバーの群れを自分で管理することだった時代に付けられた。時間はすべてを変える。
    • 私もストレージ費用の地雷を踏んだことがある。幸い数セントだったが、コンソールが関連情報を表示する方法があまりにひどく、かなり腹が立っている。
  • 大文字・小文字を区別する/しないという議論は、概してあまりに英語中心的だと感じる。
    言い換えると、特に IT では、言語に関する議論が過度に英語中心的であることがあまりにも多い。

    • 英語中心的であることは、むしろ幸いだと思う。Unicode に比べて ASCII のほうがはるかに扱いやすかったからだ。
      英語を母語としない立場から言うと、プログラミングにはすでに概念や要素が多すぎる。そこに 101 種類もの別の言語まで考慮して複雑さを増やさないほうがいい。
      Unicode とタイムゾーンは、プログラミングでより多くの言語や文化を考慮しようとする代表的な要素だが、結果として英語圏以外のプログラマーも含め、全員にとって最大級の苦痛を生んでいる。
      自分の母語でプログラムを書きたいとは思わない。その代償として、プログラミング時に主要な言語すべてを考慮しなければならないならなおさらだ。IT の議論が英語中心的なのは問題ない。多様性は複雑性であり、英語は誰かが所有する言語ではなく、人々がコミュニケーションに使う道具にすぎない。
      その共通語のおかげで、インド、中国、日本、南米など多くの人に自分の考えを伝えられる。彼らが英語で話すと決めた瞬間、彼らも英語を所有する。IT に多様性の政治を持ち込む必要はなく、技術的なままにしておくほうがいい。
    • 英語圏以外の文化の話が出たところで言うと、日本語で大文字・小文字を区別しないシステムは、ひらがなとカタカナを区別するのだろうか?
      ある意味では、この 2 種類の音節文字は大文字/小文字のアルファベットのように感じられる。
  • ほかにもいくつかある
    マルチパートアップロードは、インスタンス認証情報を持つ複数のマシンからは実行できない。主体が異なるため、互いのマルチパートアップロードにアクセスできないからだ。複数のマシンで1つのマルチパートアップロードを組み立てるには、実際の IAM ユーザーが必要になる
    LIST リクエストは遅いだけでなく、大量に行うと非常に高くつく。「bucket inventory」のような回避策はあるが、便利でも安価でもない
    バケット作成は内部的に DNS を使うため、書き込み後読み取りの一貫性がない。そのため、バケットを作った直後にアクセスできなかったり、変更の伝播を十分に待つ前には作成したばかりのバケットを削除できなかったりすることがある。https://github.com/julik/talks/blob/master/euruko-2019-no-su... を参照
    「foo」というオブジェクトと「foo/bar」というオブジェクトを同時に作成できる。すると、ファイルがディレクトリを上書きする構造になり、バケットのデータをファイルシステム構造に移植できなくなる
    S3 は大文字小文字を区別するため、ファイルシステム構造に移植できないオブジェクトを作成できる。Rails のファイルストレージは大文字小文字を区別するストレージを前提としていたため macOS で大きく壊れ、識別子を常に小文字にするよう修正された
    ほとんどの S3 設定では GET は許可するが HEAD は許可しない。オブジェクトの存在確認を防ぐための方法のようだが、確かではない。いずれにせよ、HEAD リクエストでオブジェクトサイズを確認するキャッシュフレンドリーなフローは、特に pre-signed URL では動作しない。代わりに Range をごく小さく指定した GET、たとえば最初の1バイトだけを取得するような方法で回避する必要がある
    pre-signed URL を大量に生成するなら、生成速度を10〜40倍高められる可能性がある: https://github.com/WeTransfer/wt_s3_signer
    未完了のマルチパートアップロードのストレージ料金も依然として支払うことになる。ユーザーがこのようなアップロードを開始できる構造なら特に注意が必要だ。一定時間が経過した未完了のマルチパートアップロードを自動削除する設定があるので、苦労したくなければ有効にすべきだ
    逆説的だが、S3 は革命的だったし、今でもさまざまな層で優れた製品だ。ただ、機能が多い分、落とし穴も多い

    • 数週間前に私を悩ませたのは、マルチパートアップロードの最小初期チャンクサイズ 5MiBという制限だった: https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts...
      Elixir で Stream.transform(https://hexdocs.pm/elixir/Stream.html#transform/3) を使い、列を修正して注入するストリーミング CSV 後処理パイプラインを作った。Elixir の AWS と CSV モジュールは入力のストリーミングデータは処理するが、出力ストリームの総量が 5MiB より小さいと AWS モジュールがマルチパートアップロードを使うため、S3 でエラーになって悲しかった
  • 同僚と数日かけて分析し、診断した別の面白い問題がある。S3 は単一の TCP 接続が HTTP リクエスト100個を送ると、それ以降のリクエストを静かに捨てる
    https://github.com/aws/aws-sdk-go/issues/2825

    • 静かに捨てるわけではなく、TCP 接続を閉じたというヘッダーを送る
      パフォーマンスのために keep-alive は欲しいが、クライアントが長く接続しすぎてロードバランサにホットスポットを作るのは防ぎたい、という場合によくあるパターンだ
  • S3 が標準ストレージクラスではレイテンシが高く、Web 配信に適していないという点もある
    多くの人は、画像やフォントのような Web サイトリソースを S3 から直接ホストすればよいと考えるが、ユーザー体験が悪くなる可能性がある
    “applications can achieve consistent small object latencies (and first-byte-out latencies for larger objects) of roughly 100–200 milliseconds.”
    出典: https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimi...

    • ほとんどの場合、コンテンツ配信では S3 を AWS CloudFront のオリジンとして使う
      CloudFront signed cookies を使えば、特定のユーザーに対して S3 上の本人所有コンテンツだけに CDN アクセスを与えることもできる。かなりよくできている
    • Web アセットを配信するには、通常 S3 と CloudFront を併用する
      頻繁にアクセスされるアセットをキャッシュしてレイテンシを下げ、コストもかなり削減できる
    • S3 は Web サイトの直接配信に最適化されているのではなく、ほぼ無制限のデータを耐久性高く保存し、取り出すことに最適化されている
  • アップローダーがルールを決めるというのはかなり荒っぽい。設定の甘い Web サイトなら、動機さえ十分なユーザーがユーザーコンテンツを Amazon Glacier にアップロードさせ、後でそこから配信させることもできるということだろうか?