Прямая загрузка Amazon S3 из браузера клиента - раскрытие секретного ключа

Я реализую прямую загрузку файлов с клиентской машины в Amazon S3 через REST API, используя только JavaScript, без какого-либо кода на стороне сервера. Все работает нормально, но меня беспокоит одна вещь...

Когда я отправляю запрос API-интерфейсу Amazon S3 REST, мне нужно подписать запрос и поместить подпись в заголовок Authentication. Чтобы создать подпись, я должен использовать свой секретный ключ. Но все происходит на стороне клиента, поэтому секретный ключ может быть легко обнаружен из источника страницы (даже если я обфускации/шифрования моих источников).

Как я могу справиться с этим? И это вообще проблема? Может быть, я могу ограничить использование частного ключа только для вызовов REST API из определенного CORS Origin и только для методов PUT и POST или, возможно, для ссылки только на S3 и конкретном ведре? Могут ли быть другие методы проверки подлинности?

"Безсерверное" решение идеально, но я могу рассмотреть возможность использования некоторых обработок serveride, исключая загрузку файла на мой сервер, а затем отправку на S3.

Ответ 1

Я думаю, что вы хотите загрузить с помощью браузера с помощью POST.

В принципе, вам нужен серверный код, но все, что он делает, это генерировать подписанные политики. После того, как клиентский код имеет подписанную политику, он может загружать с помощью POST непосредственно на S3 без данных, проходящих через ваш сервер.

Здесь официальные ссылки doc:

Диаграмма: 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 - не через ваш сервер.

Каждый раз, когда один из ваших пользователей хочет загрузить файл, вы должны создать POLICY и SIGNATURE на своем сервере. Вы возвращаете страницу в пользовательский браузер. Затем пользователь может загрузить файл непосредственно на S3 без прохождения вашего сервера.

Когда вы подписываете эту политику, обычно срок действия политики истекает через несколько минут. Это заставляет ваших пользователей разговаривать с вашим сервером перед загрузкой. Это позволяет отслеживать и ограничивать загрузку, если вы хотите.

Единственными данными, поступающими на ваш сервер или с вашего сервера, являются подписанные URL-адреса. Ваши секретные ключи остаются секретными на сервере.

Ответ 2

Вы можете сделать это с помощью AWS S3 Cognito, воспользовавшись этой ссылкой здесь:

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

Также попробуйте этот код

Просто измените Регион, 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>

Ответ 3

Вы говорите, что хотите "безсерверное" решение. Но это означает, что у вас нет возможности поместить какой-либо "ваш" код в цикле. (ПРИМЕЧАНИЕ. После того, как вы передадите свой код клиенту, теперь это "их" код.) Блокировка CORS не поможет: люди могут легко написать не-веб-инструмент (или веб-прокси), который добавляет правильный заголовок CORS для злоупотребления вашей системой.

Большая проблема заключается в том, что вы не можете различать разных пользователей. Вы не можете позволить одному пользователю перечислять/получать доступ к его файлам, но не позволять другим делать это. Если вы обнаружите злоупотребления, вы ничего не можете с этим поделать, кроме изменения ключа. (Вероятно, злоумышленник может просто получить снова.)

Лучше всего создать "пользователя IAM" с ключом для вашего javascript-клиента. Только дайте ему доступ на запись только к одному ведру. (но в идеале не включайте операцию ListBucket, которая сделает ее более привлекательной для злоумышленников.)

Если у вас был сервер (даже простой микро-экземпляр по цене 20 долларов США в месяц), вы могли бы подписывать ключи на своем сервере, контролируя/предотвращая злоупотребления в реальном времени. Без сервера лучшее, что вы можете сделать, это периодически следить за злоупотреблениями после факта. Вот что я буду делать:

1) периодически поворачивайте ключи для этого пользователя IAM: каждую ночь генерируйте новый ключ для этого пользователя IAM и заменяйте самый старый ключ. Поскольку есть 2 ключа, каждая клавиша будет действительна в течение 2 дней.

2) включить регистрацию S3 и загружать журналы каждый час. Установите предупреждения на "слишком много загрузок" и "слишком много загрузок". Вам нужно будет проверить как общий размер файла, так и количество загруженных файлов. И вы хотите отслеживать как глобальные итоговые значения, так и общие значения IP-адресов (с более низким порогом).

Эти проверки могут быть выполнены "без сервера", потому что вы можете запускать их на своем рабочем столе. (т.е. S3 выполняет всю работу, эти процессы только там, чтобы предупредить вас о злоупотреблении вашим ведром S3, чтобы в конце месяца вы не получали гигантский счет AWS.)

Ответ 4

Добавив дополнительную информацию к принятому ответу, вы можете обратиться к моему блогу, чтобы увидеть работающую версию кода с использованием AWS Signature version 4.

Подведу итоги здесь:

Как только пользователь выберет файл для загрузки, выполните следующие действия: 1. Выполните вызов на веб-сервере, чтобы запустить службу для генерации необходимых параметров.

  1. В этом сервисе позвоните в сервис AWS IAM, чтобы получить временный кредит

  2. Получив кредит, создайте политику сегмента (строка в кодировке 64). Затем подпишите политику корзины с временным секретным ключом доступа, чтобы сгенерировать окончательную подпись.

  3. отправить необходимые параметры обратно в интерфейс

  4. Как только это будет получено, создайте объект HTML-формы, установите необходимые параметры и отправьте его.

Для получения подробной информации, пожалуйста, обратитесь https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

Ответ 5

Чтобы создать подпись, я должен использовать свой секретный ключ. Но все происходит на стороне клиента, поэтому секретный ключ может быть легко обнаружен из источника страницы (даже если я обфускации/шифрования моих источников).

Здесь вы неправильно поняли. Сама причина, по которой используются цифровые подписи, заключается в том, что вы можете проверить что-то правильно, не раскрывая секретный ключ. В этом случае цифровая подпись используется для предотвращения изменения пользователем политики, установленной для сообщения формы.

Цифровые подписи, такие как здесь, используются для обеспечения безопасности по всему Интернету. Если бы кто-то (NSA?) Действительно смог их сломать, у них было бы намного больше целей, чем ваш ведро S3:)

Ответ 6

Если у вас нет кода на стороне сервера, ваша безопасность зависит от безопасности доступа к вашему JavaScript-коду на стороне клиента (т.е. все, у кого есть код, могут что-то выгружать).

Поэтому я бы рекомендовал просто создать специальный ведро S3, которое является общедоступным для записи (но не читаемым), поэтому вам не нужны какие-либо подписанные компоненты на стороне клиента.

Имя ведра (например, GUID) будет вашей защитой от вредоносных загрузок (но потенциальный злоумышленник не может использовать ваше ведро для передачи данных, потому что он пишет только ему)

Ответ 7

Я дал простой код для загрузки файлов из браузера 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>
      

Ответ 8

Если вы хотите использовать сторонний сервис, auth0.com поддерживает эту интеграцию. Служба auth0 обменивает стороннюю аутентификацию службы единого входа для временного токена AWS, будет ограничена разрешениями.

См: https://github.com/auth0-samples/auth0-s3-sample/
и документацию auth0.

Ответ 9

Вот как вы генерируете документ политики с использованием узла и без сервера

"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",
}

Ответ 10

@Joomler, это самый простой код, который я видел! Есть ли простой способ добавить к нему индикатор прогресса?