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


159

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

Коли я надсилаю запит на Amazon S3 REST API, мені потрібно підписати запит і поставити підпис у Authenticationзаголовку. Щоб створити підпис, я повинен використовувати свій секретний ключ. Але все відбувається на стороні клієнта, тому секретний ключ можна легко розкрити з джерела сторінки (навіть якщо я заблукаю / шифрую свої джерела).

Як я можу впоратися з цим? І це взагалі проблема? Можливо, я можу обмежити використання конкретного приватного ключа лише дзвінками API REST від конкретного походження CORS та лише методами PUT та POST або, можливо, ключ посилання лише на S3 та певне відро? Можливо, існують інші методи аутентифікації?

Рішення "без сервера" є ідеальним, але я можу розглянути можливість обробки деяких серверів, за винятком завантаження файлу на мій сервер, а потім відправки на S3.


7
Дуже просто: не зберігайте жодних секретів на стороні клієнта. Вам потрібно буде залучати сервер для підписання запиту.
Рей Ніколус

1
Ви також побачите, що підписання та кодування базових 64 цих запитів набагато простіше на стороні сервера. Здається, взагалі не залучати сюди сервер. Я можу зрозуміти, що не хочу надсилати всі байти файлів на сервер, а потім до S3, але дуже мало користі для підписання запитів на стороні клієнта, тим більше, що це буде трохи складно і потенційно повільно робити клієнтську сторону (у JavaScript).
Рей Ніколус

5
Це 2016 рік, оскільки архітектура без сервера стала досить популярною, завантаження файлів безпосередньо на S3 можливо за допомогою AWS Lambda. Дивіться мою відповідь на аналогічне запитання: stackoverflow.com/a/40828683/2504317 В основному у вас буде функція Lambda як URL-адреса, що підписує API для кожного файлу, а ваш javascript на стороні довіри просто зробіть HTTP PUT на попередньо підписаний URL. Я написав компонент Vue, що робить такі речі, код, пов’язаний із завантаженням S3, є бібліотечним агностиком, ознайомтеся і зрозумійте ідею.
KF Lin

Ще одна третя сторона для завантаження HTTP / S POST у будь-якому відрізку S3. JS3Upload чистий HTML5: jfileupload.com/products/js3upload-html5/index.html
ЄФУ

Відповіді:


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-адреси. Ваші секретні ключі залишаються секретними на сервері.


14
зауважте, що для цього використовується Signature v2, який незабаром буде замінений на v4: docs.aws.amazon.com/AmazonS3/latest/API/…
Jörn Berkefeld

9
Не забудьте додати ${filename}до ключового імені, тому для наведеного вище прикладу, user/eric/${filename}а не просто user/eric. Якщо user/ericвже існує папка, завантаження мовчки не вдасться (ви навіть будете переспрямовані до успіху__направлення), і завантажений вміст там не буде. Щойно витрачені години налагоджуючи це, думаючи, це була проблема дозволу.
Балінт Ерді

@secretmike Якщо ви отримали тайм-аут на виконання цього методу, як би ви рекомендували обійти це?
Поїздка

1
@Trip Оскільки браузер надсилає файл до S3, вам потрібно буде виявити тайм-аут у Javascript та ініціювати повторний повтор самостійно.
секретмейке

@secretmike Пахне нескінченним циклом циклу. Оскільки час очікування буде повторюватися нескінченно для будь-якого файлу понад 10 / mbs.
Поїздка

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

Чи підтримує це кілька зображень?
користувач2722667

@ user2722667 так, це так.
Joomler

@Joomler Привіт, спасибі, але я стикаюся з цією проблемою на firefox RequestTimeout. Ваше підключення до сервера не було прочитане або записане протягом часу. Непрацюючі з'єднання будуть закриті, а файл не завантажується на S3.Ви можете допомогти мені, як я можу виправити цю проблему. Дякую
usama

1
@usama чи можете ви, будь ласка, відкрити це питання в github, оскільки питання мені не зрозуміло
Joomler

@Joomler вибачте за пізню відповідь тут я відкрив питання на GitHub, будь ласка, подивіться на це спасибі. github.com/aws/aws-sdk-php/isissue/1332
usama

16

Ви говорите, що хочете рішення без сервера. Але це означає, що ви не маєте можливості ставити будь-який "свій" код у циклі. (ПРИМІТКА. Після того, як ви дасте свій код клієнтові, це вже "їхній" код.) Блокування CORS не допоможе: люди можуть легко написати не-веб-інструмент (або веб-проксі), який додає правильний заголовок CORS для зловживання вашою системою.

Велика проблема полягає в тому, що ви не можете розрізняти різних користувачів. Ви не можете дозволити одному користувачеві перелічити / отримати доступ до його файлів, але заважаєте цим робити інших. Якщо ви виявите зловживання, ви нічого не можете з цим зробити, крім зміни ключа. (Імовірно, зловмисник може просто отримати знову.)

Ваша найкраща ставка - створити "користувача IAM" з ключем для вашого клієнта javascript. Дозвольте йому написати доступ лише до одного відра. (але в ідеалі не вмикайте операцію ListBucket, що зробить її більш привабливою для зловмисників.)

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

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

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

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


3
Людина, я забув, як складні речі були перед Ламбдою.
Райан Шиллінгтон

10

Додавши додаткову інформацію до прийнятої відповіді, ви можете звернутися до мого блогу, щоб побачити працюючу версію коду, використовуючи AWS Signature версії 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
Я провів цілий день, намагаючись зрозуміти це у Javascript, і ця відповідь підказує мені, як саме це зробити за допомогою XMLhttprequest. Я дуже здивований, що ти знявся з посади. ОП попросила javascript та отримала форми в рекомендованих відповідях. Добре горе. Дякую за цю відповідь!
Пол S

У суперагента BTW є серйозні проблеми з CORS, тому здається, що xmlhttprequest є єдиним розумним способом зробити це зараз
Пол S

4

Щоб створити підпис, я повинен використовувати свій секретний ключ. Але все відбувається на стороні клієнта, тому секретний ключ можна легко розкрити з джерела сторінки (навіть якщо я заблукаю / шифрую свої джерела).

Тут ви неправильно зрозуміли. Сама причина використання цифрових підписів полягає в тому, що ви можете перевірити щось як правильне, не розкриваючи секретний ключ. У цьому випадку цифровий підпис використовується для того, щоб користувач не міг змінювати політику, встановлену для публікації форми.

Цифрові підписи, наприклад, тут, використовуються для безпеки в усьому Інтернеті. Якщо хтось (АНБ?) Справді зміг би їх зламати, вони мали б набагато більші цілі, ніж ваше відро S3 :)


2
але робот може спробувати швидко завантажувати необмежену кількість файлів. чи можу я встановити політику максимальних файлів на відро?
Dejell

3

Я дав простий код для завантаження файлів із браузера Javascript на AWS S3 і перерахування всіх файлів у відрі S3.

Кроки:

  1. Щоб дізнатися , як створити Створити IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Перейдіть на сторінку консолі Goto 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.
Нілеш Павар

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
Як я розумію - тепер у нас є для цього Когніто?
Віталій Зданевич
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.