Unggah file Amazon S3 langsung dari browser klien - pengungkapan kunci pribadi

159

Saya menerapkan unggahan file langsung dari mesin klien ke Amazon S3 melalui REST API hanya menggunakan JavaScript, tanpa kode sisi server. Semua berfungsi dengan baik tetapi satu hal yang mengkhawatirkan saya ...

Ketika saya mengirim permintaan ke Amazon S3 REST API, saya harus menandatangani permintaan dan memasukkan tanda tangan ke Authenticationheader. Untuk membuat tanda tangan, saya harus menggunakan kunci rahasia saya. Tetapi semua hal terjadi di sisi klien, jadi, kunci rahasia dapat dengan mudah diungkapkan dari sumber halaman (bahkan jika saya mengaburkan / mengenkripsi sumber saya).

Bagaimana saya bisa menangani ini? Dan apakah itu masalah? Mungkin saya dapat membatasi penggunaan kunci privat khusus hanya untuk panggilan REST API dari Origin CORS spesifik dan hanya pada metode PUT dan POST atau mungkin menghubungkan kunci hanya dengan S3 dan bucket tertentu? Mungkin ada metode otentikasi lain?

Solusi "Serverless" sangat ideal, tetapi saya dapat mempertimbangkan untuk melibatkan beberapa pemrosesan di sisi server, tidak termasuk mengunggah file ke server saya dan kemudian mengirim ke S3.

Olegas
sumber
7
Sangat sederhana: jangan menyimpan rahasia sisi klien. Anda harus melibatkan server untuk menandatangani permintaan.
Ray Nicholus
1
Anda juga akan menemukan bahwa penandatanganan dan base-64 yang menyandikan permintaan ini jauh lebih mudah dari sisi server. Tampaknya tidak masuk akal untuk melibatkan server di sini sama sekali. Saya bisa mengerti tidak ingin mengirim semua byte file ke server dan kemudian ke S3, tetapi ada sangat sedikit manfaat untuk menandatangani permintaan sisi klien, terutama karena itu akan sedikit menantang dan berpotensi lambat untuk melakukan sisi klien (dalam javascript).
Ray Nicholus
5
Ini tahun 2016, karena arsitektur tanpa server menjadi cukup populer, mengunggah file langsung ke S3 dimungkinkan dengan bantuan AWS Lambda. Lihat jawaban saya untuk pertanyaan serupa: stackoverflow.com/a/40828683/2504317 Pada dasarnya Anda akan memiliki fungsi Lambda sebagai URL yang dapat mengunggah penandatanganan API untuk setiap file, dan javascript sisi klien hanya melakukan PUT HTTP ke URL yang sudah ditandatangani. Saya sudah menulis komponen Vue melakukan hal-hal seperti itu, kode unggahan S3 yang terkait adalah agnostik perpustakaan, lihat dan dapatkan ide.
KF Lin
Pihak ke-3 lain untuk unggahan HTTP / S POST dalam ember S3 apa pun. JS3Upload pure HTML5: jfileupload.com/products/js3upload-html5/index.html
JFU

Jawaban:

216

Saya pikir yang Anda inginkan adalah Upload Berbasis Browser Menggunakan POST.

Pada dasarnya, Anda memang membutuhkan kode sisi server, tetapi yang dilakukannya hanyalah menghasilkan kebijakan yang ditandatangani. Setelah kode sisi klien memiliki kebijakan yang ditandatangani, ia dapat mengunggah menggunakan POST langsung ke S3 tanpa data melalui server Anda.

Inilah tautan resmi dokumen:

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

Kode contoh: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

Kebijakan yang ditandatangani akan masuk dalam html Anda dalam bentuk seperti ini:

<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>

Perhatikan tindakan FORMULIR mengirimkan file langsung ke S3 - bukan melalui server Anda.

Setiap kali salah satu pengguna Anda ingin mengunggah file, Anda akan membuat POLICYdan SIGNATUREdi server Anda. Anda mengembalikan halaman ke browser pengguna. Pengguna kemudian dapat mengunggah file langsung ke S3 tanpa melalui server Anda.

Saat Anda menandatangani polis, biasanya polis Anda akan kedaluwarsa setelah beberapa menit. Ini memaksa pengguna Anda untuk berbicara dengan server Anda sebelum mengunggah. Ini memungkinkan Anda memantau dan membatasi unggahan jika diinginkan.

Satu-satunya data yang masuk atau dari server Anda adalah URL yang ditandatangani. Kunci rahasia Anda tetap dirahasiakan di server.

rahasia
sumber
14
harap dicatat bahwa ini menggunakan Signature v2 yang akan segera diganti oleh v4: docs.aws.amazon.com/AmazonS3/latest/API/…
Jörn Berkefeld
9
Pastikan untuk menambahkan ${filename}nama kunci, jadi untuk contoh di atas, user/eric/${filename}bukan hanya user/eric. Jika user/ericfolder sudah ada, unggahan akan gagal secara diam-diam (Anda bahkan akan dialihkan ke success_action_redirect) dan konten yang diunggah tidak akan ada di sana. Hanya menghabiskan berjam-jam men-debug pemikiran ini adalah masalah izin.
Balint Erdi
@secretmike Jika Anda menerima batas waktu dari melakukan metode ini, bagaimana Anda akan merekomendasikan untuk mengelilingi itu?
Perjalanan
1
@ Trip Karena browser mengirim file ke S3, Anda harus mendeteksi batas waktu dalam Javascript dan memulai coba lagi sendiri.
secretmike
@secretmike Baunya seperti siklus loop tak terbatas. Karena batas waktu akan berulang tanpa batas untuk setiap file lebih dari 10 / mbs.
Perjalanan
40

Anda dapat melakukannya dengan AWS S3 Cognito, coba tautan ini di sini:

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

Coba juga kode ini

Cukup ubah Wilayah, IdentityPoolId, dan nama bucket Anda

<!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>

Untuk detail lebih lanjut, Silakan periksa - Github
Joomler
sumber
Apakah ini mendukung banyak gambar?
user2722667
@ user2722667 ya itu.
Joomler
@Joomler Hai Terima kasih, tetapi saya menghadapi masalah ini di firefox RequestTimeout Koneksi soket Anda ke server tidak dibaca dari atau ditulis dalam periode waktu habis. Koneksi siaga akan ditutup dan file tidak diunggah pada S3.Bisakah Anda membantu saya tentang bagaimana saya dapat memperbaiki masalah ini. Terima kasih
usama
1
@ usa dapat tolong buka masalah di github karena masalah tidak jelas bagi saya
Joomler
@Joomler maaf atas keterlambatan balasan di sini saya telah membuka masalah di GitHub, silakan lihat ini Terima kasih. github.com/aws/aws-sdk-php/issues/1332
usama
16

Anda mengatakan Anda menginginkan solusi "tanpa server". Tetapi itu berarti Anda tidak memiliki kemampuan untuk memasukkan kode "Anda" apa pun di loop. (CATATAN: Setelah Anda memberikan kode kepada klien, itu adalah kode "mereka" sekarang.) Mengunci CORS tidak akan membantu: Orang dapat dengan mudah menulis alat yang tidak berbasis web (atau proxy berbasis web) yang menambahkan header CORS yang benar untuk menyalahgunakan sistem Anda.

Masalah besar adalah bahwa Anda tidak dapat membedakan antara pengguna yang berbeda. Anda tidak dapat mengizinkan satu pengguna untuk membuat daftar / mengakses file-nya, tetapi mencegah orang lain melakukannya. Jika Anda mendeteksi penyalahgunaan, tidak ada yang dapat Anda lakukan selain mengubah kunci. (Yang penyerang mungkin bisa dapatkan lagi.)

Taruhan terbaik Anda adalah membuat "pengguna IAM" dengan kunci untuk klien javascript Anda. Hanya berikan akses tulis ke satu ember saja. (tapi idealnya, jangan aktifkan operasi ListBucket, yang akan membuatnya lebih menarik bagi penyerang.)

Jika Anda memiliki server (bahkan mikro instan sederhana dengan harga $ 20 / bulan), Anda bisa menandatangani kunci di server Anda sambil memantau / mencegah penyalahgunaan secara realtime. Tanpa server, yang terbaik yang dapat Anda lakukan adalah memantau penyalahgunaan secara berkala. Inilah yang akan saya lakukan:

1) secara berkala putar kunci untuk pengguna IAM itu: Setiap malam, buat kunci baru untuk pengguna IAM itu, dan ganti kunci yang tertua. Karena ada 2 kunci, setiap kunci akan berlaku selama 2 hari.

2) aktifkan S3 logging, dan unduh log setiap jam. Tetapkan lansiran pada "terlalu banyak unggahan" dan "terlalu banyak unduhan". Anda akan ingin memeriksa ukuran total file dan jumlah file yang diunggah. Dan Anda akan ingin memantau total global, dan juga total alamat per-IP (dengan ambang batas yang lebih rendah).

Pemeriksaan ini dapat dilakukan "tanpa server" karena Anda dapat menjalankannya di desktop. (Yaitu S3 melakukan semua pekerjaan, proses ini hanya ada di sana untuk mengingatkan Anda untuk penyalahgunaan ember S3 Anda sehingga Anda tidak mendapatkan tagihan AWS raksasa di akhir bulan.)

BraveNewCurrency
sumber
3
Aku lupa betapa rumitnya hal-hal sebelum Lambda.
Ryan Shillington
10

Menambahkan lebih banyak info ke jawaban yang diterima, Anda dapat merujuk ke blog saya untuk melihat versi kode yang sedang berjalan, menggunakan AWS Signature versi 4.

Akan diringkas di sini:

Segera setelah pengguna memilih file yang akan diunggah, lakukan hal berikut: 1. Melakukan panggilan ke server web untuk memulai layanan untuk menghasilkan params yang diperlukan

  1. Dalam layanan ini, lakukan panggilan ke layanan AWS IAM untuk mendapatkan kredit sementara

  2. Setelah Anda memiliki kredibilitas, buat kebijakan bucket (string 64 dikodekan basis). Kemudian tanda tangani kebijakan ember dengan kunci akses rahasia sementara untuk menghasilkan tanda tangan akhir

  3. kirim parameter yang diperlukan kembali ke UI

  4. Setelah ini diterima, buat objek bentuk html, atur params yang diperlukan dan POST.

Untuk info terperinci, silakan merujuk https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

RajeevJ
sumber
5
Saya menghabiskan sepanjang hari mencoba mencari tahu ini dalam Javascript, dan jawaban ini memberi tahu saya bagaimana melakukan ini menggunakan XMLhttprequest. Saya sangat terkejut Anda dipecat. OP meminta javascript dan mendapatkan formulir di jawaban yang direkomendasikan. Menyedihkan. Terima kasih atas jawaban ini!
Paul S
BTW superagent memiliki masalah CORS yang serius, jadi xmlhttprequest tampaknya merupakan satu-satunya cara yang masuk akal untuk melakukan ini sekarang
Paul S
4

Untuk membuat tanda tangan, saya harus menggunakan kunci rahasia saya. Tetapi semua hal terjadi di sisi klien, jadi, kunci rahasia dapat dengan mudah diungkapkan dari sumber halaman (bahkan jika saya mengaburkan / mengenkripsi sumber saya).

Di sinilah Anda salah paham. Alasan utama tanda tangan digital digunakan adalah agar Anda dapat memverifikasi sesuatu sebagai benar tanpa mengungkapkan kunci rahasia Anda. Dalam hal ini tanda tangan digital digunakan untuk mencegah pengguna dari memodifikasi kebijakan yang Anda tetapkan untuk posting bentuk.

Tanda tangan digital seperti yang ada di sini digunakan untuk keamanan di seluruh web. Jika seseorang (NSA?) Benar-benar dapat menghancurkan mereka, mereka akan memiliki target yang jauh lebih besar daripada ember S3 Anda :)

OlliM
sumber
2
tetapi robot dapat mencoba mengunggah file tanpa batas dengan cepat. dapatkah saya menetapkan kebijakan file maks per ember?
Dejell
3

Saya telah memberikan kode sederhana untuk mengunggah file dari browser Javascript ke AWS S3 dan mendaftar semua file dalam bucket S3.

Langkah:

  1. Untuk mengetahui cara membuat Buat IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Goto S3's console page dan buka konfigurasi cors dari bucket properties dan tulis kode XML berikut ini.

      <?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. Buat file HTML yang berisi kode berikut, ubah kredensial, buka file di browser, dan nikmati.

      <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>
Nilesh Pawar
sumber
2
Tidak adakah yang bisa menggunakan "IdentityPoolId" saya untuk mengunggah file ke bucket S3 saya. Bagaimana solusi ini mencegah pihak ke-3 dari hanya menyalin "IdentityPoolId" saya dan mengunggah banyak file ke bucket S3 saya?
Sahil
1
stackoverflow.com/users/4535741/sahil Anda dapat mencegah pengunggahan data / file dari domain lain dengan mengatur pengaturan CORS yang sesuai ke bucket S3. Jadi, bahkan jika ada yang mengakses id kumpulan identitas Anda, mereka tidak dapat memanipulasi file bucket s3 Anda.
Nilesh Pawar
2

Jika Anda tidak memiliki kode sisi server, keamanan Anda tergantung pada keamanan akses ke kode JavaScript Anda di sisi klien (yaitu setiap orang yang memiliki kode dapat mengunggah sesuatu).

Jadi saya akan merekomendasikan, untuk hanya membuat ember S3 khusus yang dapat ditulis untuk umum (tetapi tidak dapat dibaca), sehingga Anda tidak memerlukan komponen yang ditandatangani di sisi klien.

Nama bucket (misalnya GUID) akan menjadi satu-satunya pertahanan Anda terhadap unggahan berbahaya (tetapi penyerang potensial tidak dapat menggunakan ember Anda untuk mentransfer data, karena itu hanya ditulis untuknya)

Ruediger Jungbeck
sumber
1

Inilah cara Anda membuat dokumen kebijakan menggunakan node dan serverless

"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;

Objek konfigurasi yang digunakan disimpan di SSM Parameter Store dan terlihat seperti ini

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

Jika Anda bersedia menggunakan layanan pihak ke-3, auth0.com mendukung integrasi ini. Layanan auth0 menukar otentikasi layanan SSO pihak ke-3 untuk token sesi sementara AWS akan membatasi izin.

Lihat: https://github.com/auth0-samples/auth0-s3-sample/
dan dokumentasi auth0.

Jason
sumber
1
Seperti yang saya mengerti - sekarang kita punya Cognito untuk itu?
Vitaly Zdanevich