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

159

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

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

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

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

Olegas
источник
7
Очень просто: не храните никаких секретов на стороне клиента. Вам нужно будет привлечь сервер, чтобы подписать запрос.
Рэй Николай
1
Вы также обнаружите, что подписание и кодирование base-64 этих запросов намного проще на стороне сервера. Это не кажется неразумным, чтобы привлечь сервер здесь вообще. Я могу понять, что не хочу отправлять все файловые байты на сервер, а затем до S3, но подписывать запросы на стороне клиента очень мало пользы, тем более что это будет немного сложным и потенциально медленным для выполнения на стороне клиента. (в JavaScript).
Рэй Николай
5
Наступил 2016 год, поскольку безсерверная архитектура стала довольно популярной, загрузка файлов непосредственно на S3 возможна с помощью AWS Lambda. См. Мой ответ на похожий вопрос: stackoverflow.com/a/40828683/2504317 По сути, у вас будет функция Lambda в качестве API, подписывающего загружаемый URL-адрес для каждого файла, а ваш JavaScript-код на стороне клиента просто выполняет HTTP PUT для предварительно подписанный URL. Я написал компонент Vue, делающий такие вещи, код, связанный с загрузкой S3, не зависит от библиотеки, взгляните и получите идею.
КФ Лин
Еще одна третья сторона для загрузки HTTP / S POST в любом сегменте S3. JS3Загрузить чистый HTML5: jfileupload.com/products/js3upload-html5/index.html
JFU

Ответы:

216

Я думаю, что вы хотите, чтобы загрузки на основе браузера с использованием 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, а не через ваш сервер.

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

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

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

secretmike
источник
14
обратите внимание, что здесь используется подпись v2, которая вскоре будет заменена версией
Йорн Беркефельд
9
Обязательно добавьте ${filename}к имени ключа, так что для приведенного выше примера, user/eric/${filename}а не просто user/eric. Если user/ericэто уже существующая папка, загрузка завершится неудачно (вы даже будете перенаправлены на success_action_redirect), а загруженный контент не будет там. Просто потратил часы на отладку, думая, что это проблема разрешения.
Балинт Эрди
@secretmike Если бы вы получили тайм-аут от использования этого метода, как бы вы порекомендовали обойти это?
Поездка
1
@Trip Поскольку браузер отправляет файл на S3, вам необходимо определить время ожидания в Javascript и самостоятельно инициировать повторную попытку.
секретарм
@secretmike Это пахнет как бесконечный цикл цикла. Поскольку тайм-аут будет повторяться бесконечно для любого файла более 10 / мб.
Поездка
40

Вы можете сделать это с помощью 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>

Для более подробной информации, пожалуйста, проверьте - Github
Joomler
источник
Поддерживает ли это несколько изображений?
user2722667
@ user2722667 да, это так.
Joomler
@Joomler Привет Спасибо, но я столкнулся с этой проблемой в firefox RequestTimeout Ваше сокетное соединение с сервером не было считано или записано в течение периода ожидания. Свободные соединения будут закрыты, и файл не загружается на S3.Можете ли вы помочь мне, как я могу решить эту
проблему. Спасибо
1
@usama, не могли бы вы открыть вопрос в github, потому что проблема мне не ясна
Joomler
@Joomler извините за поздний ответ здесь я открыл вопрос на GitHub, пожалуйста, посмотрите на это Спасибо. github.com/aws/aws-sdk-php/issues/1332
усама
16

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

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

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

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

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

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

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

BraveNewCurrency
источник
3
Чувак, я забыл, как все было до Лямбды
Райан Шиллингтон
10

Добавив дополнительную информацию к принятому ответу, вы можете обратиться к моему блогу, чтобы увидеть работающую версию кода с использованием 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/

RajeevJ
источник
5
Я провел целый день, пытаясь выяснить это в Javascript, и этот ответ говорит мне, как именно это сделать с помощью XMLhttprequest. Я очень удивлен, что ты получил отрицательный голос. ОП попросил javascript и получил формы в рекомендуемых ответах. Печаль во благо. Спасибо за этот ответ!
Пол С
Кстати, у суперагента серьезные проблемы с CORS, поэтому xmlhttprequest кажется единственным разумным способом сделать это прямо сейчас
Paul S
4

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

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

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

OlliM
источник
2
но робот может попытаться загрузить неограниченное количество файлов быстро. я могу установить политику максимальных файлов на ведро?
Дежелл
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
Неужели никто не сможет использовать мой «IdentityPoolId» для загрузки файлов в мою корзину S3. Как это решение препятствует тому, чтобы третьи лица просто копировали мой «IdentityPoolId» и загружали много файлов в мое хранилище S3?
Сахил
1
stackoverflow.com/users/4535741/sahil Вы можете предотвратить загрузку данных / файлов из других доменов, установив соответствующие настройки CORS для корзины S3. Поэтому, даже если кто-то получит доступ к вашему идентификатору пула идентификаторов, он не сможет манипулировать вашими файлами S3 Bucket.
Нилеш Павар
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 для этого?
Виталий Зданевич