Amazon S3クライアントブラウザーからの直接ファイルアップロード-秘密キーの開示


159

サーバー側のコードなしで、JavaScriptのみを使用してREST API経由でクライアントマシンからAmazon S3にファイルを直接アップロードすることを実装しています。すべて正常に動作しますが、心配なことは...

Amazon S3 REST APIにリクエストを送信するとき、リクエストに署名し、Authenticationヘッダーに署名を付ける必要があります。署名を作成するには、秘密鍵を使用する必要があります。しかし、すべてのことはクライアント側で行われるため、秘密鍵はページのソースから簡単に明らかにできます(ソースを難読化/暗号化した場合でも)。

どうすればこれを処理できますか?そしてそれはまったく問題ですか?特定の秘密鍵の使用を特定のCORSオリジンからのREST API呼び出しとPUTおよびPOSTメソッドのみに制限したり、リンクキーをS3と特定のバケットのみに制限したりできますか?別の認証方法があるかもしれませんか?

「サーバーレス」ソリューションが理想的ですが、サーバーへのファイルのアップロードとS3への送信を除いて、サーバーサイドでの処理を検討することができます。


7
非常に単純です。クライアント側にシークレットを保存しないでください。リクエストに署名するには、サーバーを関与させる必要があります。
レイニコラス2013

1
また、これらのリクエストの署名とbase-64エンコーディングは、サーバー側の方がはるかに簡単です。ここにサーバーを関与させることは不合理に思われません。すべてのファイルバイトをサーバーに送信してからS3まで送信したくないことは理解できますが、クライアント側でリクエストに署名しても、クライアント側で少しやりがいがあり、遅くなる可能性があるため、ほとんどメリットがありません。 (JavaScript)。
レイニコラス2013

5
サーバーレスアーキテクチャの人気が高まった2016年です。AWSLambdaを使用すると、S3にファイルを直接アップロードできます 。同様の質問への私の回答をご覧ください:stackoverflow.com/a/40828683/2504317基本的に、各ファイルのアップロード可能なURLに署名するAPIとしてLambda関数があり、クライアント側のJavaScriptはHTTP PUTを実行するだけです署名済みURL。私はそのようなことをするVueコンポーネントを書きました、S3アップロード関連のコードはライブラリにとらわれず、一見してアイデアを得ます。
KFリン

任意のS3バケットでのHTTP / S POSTアップロード用の別のサードパーティ。JS3UploadピュアHTML5:jfileupload.com/products/js3upload-html5/index.html
JFU

回答:


215

POSTを使用したブラウザベースのアップロードが必要だと思います。

基本的に、サーバー側のコードが必要ですが、必要なのは署名されたポリシーを生成することだけです。クライアント側のコードに署名済みポリシーが設定されると、データはサーバーを経由せずに、POSTを使用してS3に直接アップロードできます。

ここに公式のドキュメントリンクがあります:

図:http : //docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html

コード例:http : //docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

署名されたポリシーは、次のような形式でHTMLに記述されます。

<html>
  <head>
    ...
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    ...
  </head>
  <body>
  ...
  <form action="http://johnsmith.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
    Key to upload: <input type="input" name="key" value="user/eric/" /><br />
    <input type="hidden" name="acl" value="public-read" />
    <input type="hidden" name="success_action_redirect" value="http://johnsmith.s3.amazonaws.com/successful_upload.html" />
    Content-Type: <input type="input" name="Content-Type" value="image/jpeg" /><br />
    <input type="hidden" name="x-amz-meta-uuid" value="14365123651274" />
    Tags for File: <input type="input" name="x-amz-meta-tag" value="" /><br />
    <input type="hidden" name="AWSAccessKeyId" value="AKIAIOSFODNN7EXAMPLE" />
    <input type="hidden" name="Policy" value="POLICY" />
    <input type="hidden" name="Signature" value="SIGNATURE" />
    File: <input type="file" name="file" /> <br />
    <!-- The elements after this will be ignored -->
    <input type="submit" name="submit" value="Upload to Amazon S3" />
  </form>
  ...
</html>

FORMアクションがファイルサーバー経由ではなくS3に直接送信していることに注意してください。

ユーザーのたびに1は、ファイルをアップロードしたい、あなたが作成しPOLICYSIGNATURE、サーバー上。ユーザーのブラウザにページを返します。その後、ユーザーはサーバーを経由せずにファイルを直接S3にアップロードできます。

ポリシーに署名すると、通常は数分後にポリシーが期限切れになります。これにより、ユーザーはアップロードする前にサーバーと通信する必要があります。これにより、必要に応じてアップロードを監視および制限できます。

サーバーとの間でやり取りされる唯一のデータは、署名付きURLです。秘密鍵はサーバー上で秘密にされます。


14
:すぐV4に置き換えられます。この用途署名v2のことに注意してくださいdocs.aws.amazon.com/AmazonS3/latest/API/...
ジョーンBerkefeld

9
${filename}キー名に必ず追加してください。上記の例では、のuser/eric/${filename}代わりにuser/eric。場合はuser/eric、すでに既存のフォルダで、アップロードは黙って失敗します(あなたもsuccess_action_redirectにリダイレクトされます)とアップロードされたコンテンツが存在しません。これを許可の問題だと考えてこのデバッグに何時間も費やしただけです。
Balint Erdi 2015年

@secretmikeこの方法でタイムアウトが発生した場合、どのように迂回することをお勧めしますか?
旅行

1
@TripブラウザがファイルをS3に送信しているため、JavaScriptでタイムアウトを検出し、自分で再試行を開始する必要があります。
secretmike 2016年

@secretmike無限ループサイクルのようなにおいがします。タイムアウトは、10 / mbsを超えるファイルに対して無期限に繰り返されるためです。
2016年

40

AWS S3 Cognitoでこれを行うには、次のリンクを試してください。

http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-examples.html#Amazon_S3

このコードも試してください

Region、IdentityPoolId、バケット名を変更するだけです

<!DOCTYPE html>
<html>

<head>
    <title>AWS S3 File Upload</title>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</head>

<body>
    <input type="file" id="file-chooser" />
    <button id="upload-button">Upload to S3</button>
    <div id="results"></div>
    <script type="text/javascript">
    AWS.config.region = 'your-region'; // 1. Enter your region

    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: 'your-IdentityPoolId' // 2. Enter your identity pool
    });

    AWS.config.credentials.get(function(err) {
        if (err) alert(err);
        console.log(AWS.config.credentials);
    });

    var bucketName = 'your-bucket'; // Enter your bucket name
    var bucket = new AWS.S3({
        params: {
            Bucket: bucketName
        }
    });

    var fileChooser = document.getElementById('file-chooser');
    var button = document.getElementById('upload-button');
    var results = document.getElementById('results');
    button.addEventListener('click', function() {

        var file = fileChooser.files[0];

        if (file) {

            results.innerHTML = '';
            var objKey = 'testing/' + file.name;
            var params = {
                Key: objKey,
                ContentType: file.type,
                Body: file,
                ACL: 'public-read'
            };

            bucket.putObject(params, function(err, data) {
                if (err) {
                    results.innerHTML = 'ERROR: ' + err;
                } else {
                    listObjs();
                }
            });
        } else {
            results.innerHTML = 'Nothing to upload.';
        }
    }, false);
    function listObjs() {
        var prefix = 'testing';
        bucket.listObjects({
            Prefix: prefix
        }, function(err, data) {
            if (err) {
                results.innerHTML = 'ERROR: ' + err;
            } else {
                var objKeys = "";
                data.Contents.forEach(function(obj) {
                    objKeys += obj.Key + "<br>";
                });
                results.innerHTML = objKeys;
            }
        });
    }
    </script>
</body>

</html>

詳細については、確認してください-Github

これは複数の画像をサポートしていますか?
user2722667 2017年

@ user2722667はい、あります。
Joomler 2017年

@Joomlerこんにちはありがとうございます。Firefoxでこの問題に直面していますRequestTimeoutサーバーへのソケット接続は、タイムアウト期間内に読み書きされませんでした。アイドル接続は閉じられ、ファイルはS3にアップロードされません。この問題を解決する方法を教えてください。ありがとう
usama

1
@usama問題は私には明らかではないため、githubで問題を開いてください
Joomler

@Joomler返信が遅くなって申し訳ありません。GitHubで問題を解決しました。この感謝をご覧ください。github.com/aws/aws-sdk-php/issues/1332
usama 2017

16

「サーバーレス」ソリューションが必要だと言っています。しかし、これは、「自分の」コードをループに入れる能力がないことを意味します。(注:コードをクライアントに渡すと、それは「その」コードになります。)CORSをロックしても効果はありません。Webベース以外のツール(またはWebベースのプロキシ)を簡単に記述して、システムを悪用する正しいCORSヘッダー。

大きな問題は、異なるユーザーを区別できないことです。1人のユーザーに自分のファイルのリスト/アクセスを許可することはできませんが、他のユーザーがそうすることはできません。不正行為を検出した場合は、キーを変更する以外に対処法はありません。(攻撃者がおそらく再び得ることができるものです。)

あなたの最善の策は、あなたのjavascriptクライアントのキーで「IAMユーザー」を作成することです。1つのバケットへの書き込みアクセスのみを許可します。(ただし、理想的には、ListBucket操作を有効にしないでください。攻撃者にとって魅力的なものになります。)

サーバーがある場合(単純なマイクロインスタンスでも月額$ 20)、リアルタイムで悪用を監視/防止しながらサーバーのキーに署名できます。サーバーがない場合、できる限り最善の方法は、事後の悪用を定期的に監視することです。これが私がすることです:

1)そのIAMユーザーのキーを定期的にローテーションします。毎晩、そのIAMユーザーの新しいキーを生成し、最も古いキーを置き換えます。キーは2つあるため、各キーは2日間有効です。

2)S3ログを有効にし、ログを1時間ごとにダウンロードします。「アップロードが多すぎます」と「ダウンロードが多すぎます」に関するアラートを設定します。合計ファイルサイズとアップロードされたファイルの数の両方を確認する必要があります。また、グローバル合計とIPアドレスごとの合計の両方を監視する必要があります(しきい値は低くなります)。

これらのチェックはデスクトップで実行できるため、「サーバーレス」で実行できます。(つまり、S3はすべての作業を行います。これらのプロセスはS3バケットの乱用を警告するためにそこにあるだけなので、月末に巨大な AWS請求書を取得しません。)


3
ラムダの前は複雑なことを忘れていました。
Ryan Shillington

10

受け入れられた回答にさらに情報を追加します。AWS署名バージョン4を使用して、私のブログを参照し、実行中のコードのバージョンを確認できます。

ここで要約します:

ユーザーがアップロードするファイルを選択したらすぐに、次の手順を実行します。1. Webサーバーを呼び出して、必要なパラメーターを生成するサービスを開始します

  1. このサービスでは、AWS IAMサービスを呼び出して一時的な認証を取得します

  2. credを取得したら、バケットポリシー(base 64エンコードされた文字列)を作成します。次に、一時的なシークレットアクセスキーでバケットポリシーに署名し、最終的な署名を生成します

  3. 必要なパラメータをUIに送り返す

  4. これを受け取ったら、htmlフォームオブジェクトを作成し、必要なパラメーターを設定してPOSTします。

詳細については、https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/を参照して ください。


5
私はこれをJavascriptで理解しようと丸一日費やしましたが、この答えは、XMLhttprequestを使用してこれを行う方法を正確に教えてくれます。あなたが反対票を投じられたことに私は非常に驚いています。OPはJavaScriptを要求し、推奨される回答のフォームを取得しました。やれやれだぜ。この回答をありがとう!
ポールS

BTWスーパーエージェントには深刻なCORSの問題があるため、現在xmlhttprequestがこれを行う唯一の合理的な方法であるようです
Paul S

4

署名を作成するには、秘密鍵を使用する必要があります。しかし、すべてのことはクライアント側で行われるため、秘密鍵はページのソースから簡単に明らかにできます(ソースを難読化/暗号化した場合でも)。

これはあなたが誤解しているところです。デジタル署名が使用される主な理由は、秘密鍵を明かさずに正しいものを検証できるようにするためです。この場合、デジタル署名は、フォーム投稿に設定したポリシーをユーザーが変更できないようにするために使用されます。

このようなデジタル署名は、Web全体のセキュリティのために使用されます。もし誰か(NSA?)が本当にそれらを破ることができたなら、彼らはあなたのS3バケットよりもはるかに大きなターゲットを持っているでしょう:)


2
しかし、ロボットは無制限のファイルをすばやくアップロードしようとする場合があります。バケットごとの最大ファイル数のポリシーを設定できますか?
Dejell 2016年

3

JavaScriptブラウザーからAWS S3にファイルをアップロードし、S3バケット内のすべてのファイルをリストする簡単なコードを指定しました。

手順:

  1. Create IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.htmlの作成方法を知るには

    1. S3のコンソールページに移動し、バケットのプロパティからcors構成を開き、それに次のXMLコードを書き込みます。

      <?xml version="1.0" encoding="UTF-8"?>
      <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
       <CORSRule>    
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedMethod>HEAD</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
       </CORSRule>
      </CORSConfiguration>
    2. 次のコードを含むHTMLファイルを作成して資格情報を変更し、ブラウザでファイルを開いてお楽しみください。

      <script type="text/javascript">
       AWS.config.region = 'ap-north-1'; // Region
       AWS.config.credentials = new AWS.CognitoIdentityCredentials({
       IdentityPoolId: 'ap-north-1:*****-*****',
       });
       var bucket = new AWS.S3({
       params: {
       Bucket: 'MyBucket'
       }
       });
      
       var fileChooser = document.getElementById('file-chooser');
       var button = document.getElementById('upload-button');
       var results = document.getElementById('results');
      
       function upload() {
       var file = fileChooser.files[0];
       console.log(file.name);
      
       if (file) {
       results.innerHTML = '';
       var params = {
       Key: n + '.pdf',
       ContentType: file.type,
       Body: file
       };
       bucket.upload(params, function(err, data) {
       results.innerHTML = err ? 'ERROR!' : 'UPLOADED.';
       });
       } else {
       results.innerHTML = 'Nothing to upload.';
       }    }
      </script>
      <body>
       <input type="file" id="file-chooser" />
       <input type="button" onclick="upload()" value="Upload to S3">
       <div id="results"></div>
      </body>

2
S3バケットにファイルをアップロードするために誰も私の「IdentityPoolId」を使用することはできませんか?このソリューションでは、サードパーティが「IdentityPoolId」をコピーして大量のファイルをS3バケットにアップロードすることをどのように防止していますか?
Sahil

1
stackoverflow.com/users/4535741/sahil適切なCORS設定をS3バケットに設定することにより、他のドメインからのデータ/ファイルのアップロードを防ぐことができます。そのため、誰かがIDプールIDにアクセスしたとしても、s3バケットファイルを操作することはできません。
Nilesh Pawar

2

サーバー側のコードがない場合、セキュリティはクライアント側のJavaScriptコードへのアクセスのセキュリティに依存します(つまり、コードを持っている人はだれでも何かをアップロードできます)。

そのため、クライアント側で署名付きコンポーネントを必要としないように、パブリックに書き込み可能(ただし読み取り不可)である特別なS3バケットを作成することをお勧めします。

バケット名(GUIDなど)は、悪意のあるアップロードに対する唯一の防御手段になります(ただし、潜在的な攻撃者はバケットにのみ書き込みを行うため、バケットを使用してデータを転送することはできません)。


1

ノードとサーバーレスを使用してポリシードキュメントを生成する方法は次のとおりです

"use strict";

const uniqid = require('uniqid');
const crypto = require('crypto');

class Token {

    /**
     * @param {Object} config SSM Parameter store JSON config
     */
    constructor(config) {

        // Ensure some required properties are set in the SSM configuration object
        this.constructor._validateConfig(config);

        this.region = config.region; // AWS region e.g. us-west-2
        this.bucket = config.bucket; // Bucket name only
        this.bucketAcl = config.bucketAcl; // Bucket access policy [private, public-read]
        this.accessKey = config.accessKey; // Access key
        this.secretKey = config.secretKey; // Access key secret

        // Create a really unique videoKey, with folder prefix
        this.key = uniqid() + uniqid.process();

        // The policy requires the date to be this format e.g. 20181109
        const date = new Date().toISOString();
        this.dateString = date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2);

        // The number of minutes the policy will need to be used by before it expires
        this.policyExpireMinutes = 15;

        // HMAC encryption algorithm used to encrypt everything in the request
        this.encryptionAlgorithm = 'sha256';

        // Client uses encryption algorithm key while making request to S3
        this.clientEncryptionAlgorithm = 'AWS4-HMAC-SHA256';
    }

    /**
     * Returns the parameters that FE will use to directly upload to s3
     *
     * @returns {Object}
     */
    getS3FormParameters() {
        const credentialPath = this._amazonCredentialPath();
        const policy = this._s3UploadPolicy(credentialPath);
        const policyBase64 = new Buffer(JSON.stringify(policy)).toString('base64');
        const signature = this._s3UploadSignature(policyBase64);

        return {
            'key': this.key,
            'acl': this.bucketAcl,
            'success_action_status': '201',
            'policy': policyBase64,
            'endpoint': "https://" + this.bucket + ".s3-accelerate.amazonaws.com",
            'x-amz-algorithm': this.clientEncryptionAlgorithm,
            'x-amz-credential': credentialPath,
            'x-amz-date': this.dateString + 'T000000Z',
            'x-amz-signature': signature
        }
    }

    /**
     * Ensure all required properties are set in SSM Parameter Store Config
     *
     * @param {Object} config
     * @private
     */
    static _validateConfig(config) {
        if (!config.hasOwnProperty('bucket')) {
            throw "'bucket' is required in SSM Parameter Store Config";
        }
        if (!config.hasOwnProperty('region')) {
            throw "'region' is required in SSM Parameter Store Config";
        }
        if (!config.hasOwnProperty('accessKey')) {
            throw "'accessKey' is required in SSM Parameter Store Config";
        }
        if (!config.hasOwnProperty('secretKey')) {
            throw "'secretKey' is required in SSM Parameter Store Config";
        }
    }

    /**
     * Create a special string called a credentials path used in constructing an upload policy
     *
     * @returns {String}
     * @private
     */
    _amazonCredentialPath() {
        return this.accessKey + '/' + this.dateString + '/' + this.region + '/s3/aws4_request';
    }

    /**
     * Create an upload policy
     *
     * @param {String} credentialPath
     *
     * @returns {{expiration: string, conditions: *[]}}
     * @private
     */
    _s3UploadPolicy(credentialPath) {
        return {
            expiration: this._getPolicyExpirationISODate(),
            conditions: [
                {bucket: this.bucket},
                {key: this.key},
                {acl: this.bucketAcl},
                {success_action_status: "201"},
                {'x-amz-algorithm': 'AWS4-HMAC-SHA256'},
                {'x-amz-credential': credentialPath},
                {'x-amz-date': this.dateString + 'T000000Z'}
            ],
        }
    }

    /**
     * ISO formatted date string of when the policy will expire
     *
     * @returns {String}
     * @private
     */
    _getPolicyExpirationISODate() {
        return new Date((new Date).getTime() + (this.policyExpireMinutes * 60 * 1000)).toISOString();
    }

    /**
     * HMAC encode a string by a given key
     *
     * @param {String} key
     * @param {String} string
     *
     * @returns {String}
     * @private
     */
    _encryptHmac(key, string) {
        const hmac = crypto.createHmac(
            this.encryptionAlgorithm, key
        );
        hmac.end(string);

        return hmac.read();
    }

    /**
     * Create an upload signature from provided params
     * https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html#signing-request-intro
     *
     * @param policyBase64
     *
     * @returns {String}
     * @private
     */
    _s3UploadSignature(policyBase64) {
        const dateKey = this._encryptHmac('AWS4' + this.secretKey, this.dateString);
        const dateRegionKey = this._encryptHmac(dateKey, this.region);
        const dateRegionServiceKey = this._encryptHmac(dateRegionKey, 's3');
        const signingKey = this._encryptHmac(dateRegionServiceKey, 'aws4_request');

        return this._encryptHmac(signingKey, policyBase64).toString('hex');
    }
}

module.exports = Token;

使用される設定オブジェクトはSSM パラメータストアに格納され、次のようになります。

{
    "bucket": "my-bucket-name",
    "region": "us-west-2",
    "bucketAcl": "private",
    "accessKey": "MY_ACCESS_KEY",
    "secretKey": "MY_SECRET_ACCESS_KEY",
}

0

サードパーティのサービスを使用する場合は、auth0.comがこの統合をサポートしています。auth0サービスは、サードパーティのSSOサービス認証をAWS一時セッショントークンと交換して、アクセス許可を制限します。

https://github.com/auth0-samples/auth0-s3-sample/
およびauth0のドキュメントを参照してください 。


1
私が理解しているように-今はそのためのCognitoがありますか?
Vitaly Zdanevich 2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.