Caricamento diretto dei file Amazon S3 dal browser del client - divulgazione della chiave privata


159

Sto implementando un caricamento di file diretto dal computer client su Amazon S3 tramite l'API REST utilizzando solo JavaScript, senza alcun codice lato server. Tutto funziona bene ma una cosa mi preoccupa ...

Quando invio una richiesta all'API REST di Amazon S3, devo firmare la richiesta e inserire una firma Authenticationnell'intestazione. Per creare una firma, devo usare la mia chiave segreta. Ma tutto accade sul lato client, quindi la chiave segreta può essere facilmente rivelata dall'origine della pagina (anche se offusco / crittografo le mie fonti).

Come posso gestirlo? Ed è un problema? Forse posso limitare l'uso specifico della chiave privata solo alle chiamate API REST da una specifica origine CORS e solo ai metodi PUT e POST o forse collegare la chiave solo a S3 e bucket specifici? Potrebbero esserci altri metodi di autenticazione?

La soluzione "Serverless" è l'ideale, ma posso prendere in considerazione la possibilità di coinvolgere alcune elaborazioni lato server, escludendo il caricamento di un file sul mio server e quindi l'invio a S3.


7
Molto semplice: non memorizzare segreti sul lato client. Dovrai coinvolgere un server per firmare la richiesta.
Ray Nicholus,

1
Scoprirai inoltre che la firma e la codifica base 64 di queste richieste è molto più semplice sul lato server. Non sembra irragionevole coinvolgere un server qui. Posso capire di non voler inviare tutti i byte di file a un server e quindi fino a S3, ma ci sono pochissimi vantaggi nel firmare le richieste sul lato client, soprattutto perché sarà un po 'impegnativo e potenzialmente lento nel fare lato client (in javascript).
Ray Nicholus,

5
È il 2016, quando l'architettura senza server è diventata molto popolare, è possibile caricare file direttamente su S3 con l'aiuto di AWS Lambda. Vedi la mia risposta a una domanda simile: stackoverflow.com/a/40828683/2504317 Fondamentalmente avresti una funzione Lambda come API che firma URL in grado di caricare per ogni file e il tuo javascript sul lato client esegue semplicemente un HTTP PUT al URL pre-firmato. Ho scritto un componente Vue facendo queste cose, il codice relativo al caricamento S3 è indipendente dalla libreria, dai un'occhiata e prendi l'idea.
KF Lin,

Un'altra terza parte per il caricamento HTTP / S POST in qualsiasi bucket S3. JS3Upload HTML5 puro: jfileupload.com/products/js3upload-html5/index.html
JFU

Risposte:


216

Penso che ciò che desideri siano i caricamenti basati su browser tramite POST.

Fondamentalmente, è necessario il codice lato server, ma tutto ciò che fa è generare criteri firmati. Una volta che il codice lato client ha la politica firmata, può caricare utilizzando POST direttamente su S3 senza che i dati passino attraverso il server.

Ecco i collegamenti doc ufficiali:

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

Codice di esempio: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

La politica firmata andrebbe nel tuo html in una forma come questa:

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

Si noti che l'azione FORM invia il file direttamente a S3 , non tramite il server.

Ogni volta che uno dei tuoi utenti desidera caricare un file, crei il POLICYe SIGNATUREsul tuo server. Si restituisce la pagina al browser dell'utente. L'utente può quindi caricare un file direttamente su S3 senza passare attraverso il server.

Quando si firma la politica, in genere la politica scade dopo alcuni minuti. Ciò costringe i tuoi utenti a parlare con il tuo server prima del caricamento. Ciò ti consente di monitorare e limitare i caricamenti, se lo desideri.

Gli unici dati che vanno al o dal tuo server sono gli URL firmati. Le tue chiavi segrete rimangono segrete sul server.


14
si prega di notare che questo utilizza Signature v2 che sarà presto sostituito da v4: docs.aws.amazon.com/AmazonS3/latest/API/…
Jörn Berkefeld

9
Assicurati di aggiungere ${filename}al nome della chiave, quindi per l'esempio sopra, user/eric/${filename}invece che semplicemente user/eric. Se user/ericè già presente una cartella, il caricamento fallirà silenziosamente (verrai anche reindirizzato a success_action_redirect) e il contenuto caricato non sarà presente. Ho passato ore a eseguire il debug di questo pensiero pensando che fosse un problema di autorizzazione.
Balint Erdi,

@secretmike Se hai ricevuto un timeout da questo metodo, come consiglieresti di aggirarlo?
Viaggio del

1
@Trip Poiché il browser sta inviando il file a S3, dovrai rilevare il timeout in Javascript e avviare un nuovo tentativo da solo.
Secretmike

@secretmike Che odora di ciclo infinito. Poiché il timeout si ripeterà indefinitamente per qualsiasi file superiore a 10 / mbs.
Viaggio

40

Puoi farlo da AWS S3 Cognito prova questo link qui:

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

Prova anche questo codice

Basta cambiare Region, IdentityPoolId e il tuo nome bucket

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

Per maggiori dettagli, controlla - Github

Questo supporta più immagini?
user2722667

@ user2722667 sì.
Joomler,

@Joomler Ciao Grazie ma sto affrontando questo problema su Firefox RequestTimeout La connessione del socket al server non è stata letta o scritta entro il periodo di timeout. Le connessioni inattive verranno chiuse e il file non verrà caricato su S3. Puoi aiutarmi per favore su come posso risolvere questo problema. Grazie
usama,

1
@usama puoi per favore aprire il problema nel github perché il problema non mi è chiaro
Joomler

@Joomler scusa per la risposta tardiva qui ho aperto un problema su GitHub, per favore dai un'occhiata a questo Grazie. github.com/aws/aws-sdk-php/issues/1332
usama

16

Stai dicendo che vuoi una soluzione "senza server". Ciò significa che non hai la possibilità di inserire nel tuo ciclo nessuno dei "tuoi" codici. (NOTA: una volta assegnato il codice a un client, ora è il "loro" codice.) Il blocco di CORS non aiuta: le persone possono facilmente scrivere uno strumento non basato sul web (o un proxy basato sul web) che aggiunge l'intestazione CORS corretta per abusare del sistema.

Il grosso problema è che non puoi distinguere tra i diversi utenti. Non puoi consentire a un utente di elencare / accedere ai suoi file, ma impedisci ad altri di farlo. Se rilevi un abuso, non c'è nulla che tu possa fare al riguardo tranne cambiare la chiave. (Che l'attaccante può presumibilmente ottenere di nuovo.)

La soluzione migliore è creare un "utente IAM" con una chiave per il tuo client JavaScript. Dagli accesso in scrittura a un solo bucket. (ma idealmente, non abilitare l'operazione ListBucket, che lo renderà più attraente per gli attaccanti.)

Se avessi un server (anche una semplice microistanza a $ 20 / mese), potresti firmare le chiavi sul tuo server mentre monitori / previeni gli abusi in tempo reale. Senza un server, il meglio che puoi fare è monitorare periodicamente gli abusi dopo il fatto. Ecco cosa farei:

1) ruota periodicamente le chiavi per quell'utente IAM: ogni notte, genera una nuova chiave per quell'utente IAM e sostituisci la chiave più vecchia. Poiché ci sono 2 chiavi, ciascuna chiave sarà valida per 2 giorni.

2) abilitare la registrazione S3 e scaricare i registri ogni ora. Imposta avvisi su "troppi caricamenti" e "troppi download". Ti consigliamo di controllare sia la dimensione totale del file sia il numero di file caricati. E vorrai monitorare sia i totali globali, sia i totali degli indirizzi per IP (con una soglia inferiore).

Questi controlli possono essere eseguiti "senza server" perché è possibile eseguirli sul desktop. (ovvero S3 fa tutto il lavoro, questi processi sono lì solo per avvisarti di abuso del tuo bucket S3 in modo da non ricevere una fattura AWS gigante alla fine del mese.)


3
Amico, ho dimenticato quanto erano complicate le cose prima di Lambda.
Ryan Shillington,

10

Aggiungendo ulteriori informazioni alla risposta accettata, puoi fare riferimento al mio blog per vedere una versione in esecuzione del codice, utilizzando AWS Signature versione 4.

Riassumeremo qui:

Non appena l'utente seleziona un file da caricare, effettuare le seguenti operazioni: 1. Effettuare una chiamata al server Web per avviare un servizio per generare i parametri richiesti

  1. In questo servizio, effettua una chiamata al servizio AWS IAM per ottenere un credito temporaneo

  2. Una volta ottenuto il credito, creare un criterio bucket (stringa codificata base 64). Quindi firmare il criterio bucket con la chiave di accesso segreta temporanea per generare la firma finale

  3. rinvia i parametri necessari all'interfaccia utente

  4. Una volta ricevuto, crea un oggetto modulo HTML, imposta i parametri richiesti e POST.

Per informazioni dettagliate, consultare https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/


5
Ho trascorso un'intera giornata cercando di capirlo in Javascript, e questa risposta mi dice esattamente come farlo usando XMLhttprequest. Sono molto sorpreso che tu abbia ottenuto il downgrade. L'OP ha richiesto JavaScript e ha ottenuto i moduli nelle risposte consigliate. Santo cielo. Grazie per questa risposta!
Paul S

Il superagente BTW ha seri problemi CORS, quindi xmlhttprequest sembra essere l'unico modo ragionevole per farlo ora
Paul S

4

Per creare una firma, devo usare la mia chiave segreta. Ma tutto accade sul lato client, quindi la chiave segreta può essere facilmente rivelata dall'origine della pagina (anche se offusco / crittografo le mie fonti).

Questo è dove hai frainteso. Il motivo stesso per cui vengono utilizzate le firme digitali è che puoi verificare qualcosa di corretto senza rivelare la tua chiave segreta. In questo caso la firma digitale viene utilizzata per impedire all'utente di modificare la politica impostata per il post del modulo.

Firme digitali come quella qui sono utilizzate per la sicurezza in tutto il Web. Se qualcuno (NSA?) Fosse davvero in grado di romperlo, avrebbe obiettivi molto più grandi del tuo bucket S3 :)


2
ma un robot può provare a caricare rapidamente un numero illimitato di file. posso impostare una politica di max file per bucket?
Dejell

3

Ho fornito un semplice codice per caricare file dal browser Javascript su AWS S3 ed elencare tutti i file nel bucket S3.

passi:

  1. Per sapere come creare Crea IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Vai alla pagina della console di S3 e apri la configurazione cors dalle proprietà bucket e scrivi il seguente codice XML in quello.

      <?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. Crea un file HTML contenente il seguente codice cambia le credenziali, apri il file nel browser e divertiti.

      <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
Nessuno sarebbe in grado di usare il mio "IdentityPoolId" per caricare file sul mio bucket S3. In che modo questa soluzione impedisce a terzi di copiare semplicemente il mio "IdentityPoolId" e caricare molti file sul mio bucket S3?
Sahil

1
stackoverflow.com/users/4535741/sahil È possibile impedire il caricamento di dati / file da altri domini impostando le impostazioni CORS appropriate sul bucket S3. Quindi, anche se qualcuno ha avuto accesso al tuo ID del pool di identità, non può manipolare i tuoi file bucket S3.
Nilesh Pawar,

2

Se non si dispone di alcun codice lato server, la sicurezza dipende dalla sicurezza dell'accesso al codice JavaScript sul lato client (vale a dire che chiunque disponga del codice potrebbe caricare qualcosa).

Quindi consiglierei di creare semplicemente un bucket S3 speciale che sia scrivibile pubblicamente (ma non leggibile), quindi non è necessario alcun componente firmato sul lato client.

Il nome del bucket (ad esempio un GUID) sarà la tua unica difesa contro i caricamenti dannosi (ma un potenziale utente malintenzionato non potrebbe utilizzare il bucket per trasferire i dati, perché è scritto solo per lui)


1

Ecco come generare un documento di politica utilizzando il nodo e senza server

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

L'oggetto di configurazione utilizzato è archiviato nell'archivio parametri SSM e si presenta così

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

0

Se sei disposto a utilizzare un servizio di terze parti, auth0.com supporta questa integrazione. Il servizio auth0 scambia un'autenticazione del servizio SSO di terze parti con un token di sessione temporanea AWS limiterà le autorizzazioni.

Vedi: https://github.com/auth0-samples/auth0-s3-sample/
e la documentazione auth0.


1
A quanto ho capito, ora abbiamo Cognito per quello?
Vitaly Zdanevich,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.