web-dev-qa-db-fra.com

Téléchargement direct de fichier Amazon S3 à partir du navigateur client - divulgation de la clé privée

J'implémente un téléchargement direct de fichier de la machine cliente vers Amazon S3 via l'API REST en utilisant uniquement du JavaScript, sans code côté serveur. Tout fonctionne bien mais une chose m'inquiète ...

Lorsque j'envoie une demande à l'API Amazon S3 REST, je dois signer la demande et placer une signature dans l'en-tête Authentication. Pour créer une signature, je dois utiliser ma clé secrète. Mais tout se passe du côté du client, la clé secrète peut donc être facilement révélée à partir du source de la page (même si je dissimule/chiffre les sources).

Comment puis-je gérer cela? Et est-ce un problème du tout? Peut-être que je peux limiter l'utilisation d'une clé privée spécifique uniquement aux appels d'API REST à partir d'une origine CORS spécifique et uniquement aux méthodes PUT et POST ou peut-être une clé de lien vers uniquement le compartiment S3 et spécifique? Peut-être il y a d'autres méthodes d'authentification?

La solution "sans serveur" est idéale, mais je peux envisager d'impliquer certains traitements sur le serveur, à l'exclusion du téléchargement d'un fichier sur mon serveur, puis de son envoi vers S3.

132
Olegas

Je pense que ce que vous voulez, ce sont des chargements basés sur un navigateur utilisant POST.

Fondamentalement, vous avez besoin d'un code côté serveur, mais il ne génère que des stratégies signées. Une fois que le code côté client a la stratégie signée, il peut télécharger à l'aide de POST directement vers S3 sans que les données ne passent par votre serveur.

Voici les liens officiels de la doc: 

Diagramme: http://docs.aws.Amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html

Exemple de code: http://docs.aws.Amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

La politique signée irait dans votre code HTML sous la forme suivante:

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

Notez que l’action FORM envoie le fichier directement à S3 - et non par votre serveur.

Chaque fois qu'un de vos utilisateurs souhaite télécharger un fichier, vous créez les variables POLICY et SIGNATURE sur votre serveur. Vous retournez la page au navigateur de l'utilisateur. L'utilisateur peut ensuite télécharger un fichier directement sur S3 sans passer par votre serveur.

Lorsque vous signez la stratégie, vous la faites expirer après quelques minutes. Cela oblige vos utilisateurs à parler à votre serveur avant de télécharger. Cela vous permet de surveiller et de limiter les téléchargements si vous le souhaitez.

Les seules données en provenance ou à destination de votre serveur sont les URL signées. Vos clés secrètes restent secrètes sur le serveur.

195
secretmike

Vous dites que vous voulez une solution "sans serveur". Mais cela signifie que vous n’avez aucune possibilité de mettre aucun de vos "codes" dans la boucle. (REMARQUE: une fois que vous donnez votre code à un client, c'est "leur" code maintenant.) Verrouiller CORS ne va pas aider: les gens peuvent facilement écrire un outil non basé sur le Web (ou un proxy basé sur le Web) qui ajoute l'en-tête CORS correct pour abuser de votre système.

Le gros problème est que vous ne pouvez pas différencier les différents utilisateurs. Vous ne pouvez pas autoriser un utilisateur à répertorier/accéder à ses fichiers, mais empêcher les autres de le faire. Si vous détectez un abus, vous ne pouvez rien faire à part changer la clé. (Ce que l'attaquant peut vraisemblablement simplement récupérer.)

Votre meilleur choix est de créer un "utilisateur IAM" avec une clé pour votre client javascript. Donnez-lui seulement un accès en écriture à un seul compartiment. (mais idéalement, n'activez pas l'opération ListBucket, cela la rendrait plus attrayante pour les attaquants.)

Si vous aviez un serveur (même une simple micro-instance à 20 USD/mois), vous pouvez signer les clés sur votre serveur tout en surveillant/empêchant les abus en temps réel. Sans serveur, le mieux que vous puissiez faire est de contrôler périodiquement les abus après coup. Voici ce que je ferais:

1) Faites régulièrement pivoter les clés pour cet utilisateur IAM: Chaque nuit, générez une nouvelle clé pour cet utilisateur IAM et remplacez la clé la plus ancienne. Puisqu'il y a 2 clés, chaque clé sera valide pour 2 jours.

2) activer la journalisation S3 et télécharger les journaux toutes les heures. Définir des alertes sur "trop ​​de téléchargements" et "trop ​​de téléchargements". Vous voudrez vérifier la taille totale du fichier et le nombre de fichiers téléchargés. Et vous voudrez surveiller les totaux globaux, ainsi que les totaux par adresse IP (avec un seuil inférieur).

Ces vérifications peuvent être effectuées "sans serveur" car vous pouvez les exécuter sur votre bureau. (c’est-à-dire que S3 fait tout le travail, ces processus ne sont là que pour vous alerter d’abus de votre panier S3 afin que vous n’ayez pas une facture géant AWS à la fin du mois.)

15
BraveNewCurrency

En ajoutant plus d’informations à la réponse acceptée, vous pouvez vous référer à mon blog pour afficher une version en cours du code, à l’aide de AWS Signature version 4.

Résumera ici:

Dès que l’utilisateur sélectionne un fichier à télécharger, procédez comme suit: 1. Appelez le serveur Web pour lancer un service afin de générer les paramètres requis

  1. Dans ce service, appelez le service AWS IAM pour obtenir un identifiant temporaire.

  2. Une fois que vous avez le credo, créez une stratégie de compartiment (chaîne codée en base 64). Ensuite, signez la stratégie de compartiment avec la clé d'accès secrète temporaire pour générer la signature finale.

  3. renvoyer les paramètres nécessaires à l'interface utilisateur

  4. Une fois que cela est reçu, créez un objet de formulaire html, définissez les paramètres requis et POST.

Pour plus d'informations, référez-vous s'il vous plaît https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

8
RajeevJ

Pour créer une signature, je dois utiliser ma clé secrète. Mais toutes choses se passe du côté du client, la clé secrète peut donc être facilement révélée depuis la source de la page (même si j'obscurcir/chiffrer mes sources).

C'est là que vous avez mal compris. La raison principale pour laquelle les signatures numériques sont utilisées est que vous pouvez vérifier que quelque chose est correct sans révéler votre clé secrète. Dans ce cas, la signature numérique est utilisée pour empêcher l'utilisateur de modifier la stratégie que vous avez définie pour la publication du formulaire.

Les signatures numériques telles que celle-ci sont utilisées pour la sécurité sur le Web. Si quelqu'un (NSA?) Était vraiment capable de le briser, il aurait des cibles beaucoup plus grandes que votre compartiment S3 :)

4
OlliM

Si vous n'avez pas de code côté serveur, votre sécurité dépend de la sécurité de l'accès à votre code JavaScript côté client (c'est-à-dire que toute personne possédant le code peut télécharger quelque chose).

Je vous recommande donc de créer simplement un compartiment S3 spécial, accessible en écriture (mais non lisible), de sorte que vous n’ayez pas besoin de composants signés côté client.

Le nom du compartiment (un GUID, par exemple) constituera votre seule défense contre les téléchargements malveillants (mais un attaquant potentiel ne pourrait pas utiliser votre compartiment pour transférer des données, car il s'agit d'écrire uniquement pour lui).

2
Ruediger Jungbeck

J'ai donné un code simple pour télécharger des fichiers du navigateur Javascript vers AWS S3 et répertorier tous les fichiers dans le compartiment S3.

Pas:

  1. Pour savoir comment créer Create IdentityPoolId http://docs.aws.Amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Allez à la page de console de S3 et ouvrez la configuration à partir des propriétés du compartiment et écrivez le code XML suivant dans celle-ci.

      <?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. Créez un fichier HTML contenant le code suivant, modifiez les informations d'identification, ouvrez le fichier dans le navigateur et profitez-en.

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

Voici comment vous générez un document de politique en utilisant node et 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;

L'objet de configuration utilisé est stocké dans SSM Parameter Store et ressemble à ceci

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

Si vous souhaitez utiliser un service tiers, auth0.com prend en charge cette intégration. Le service auth0 échange une authentification de service d'authentification unique tierce pour un jeton de session temporaire AWS avec des autorisations limitées.

Voir: https://github.com/auth0-samples/auth0-s3-sample/
et la documentation auth0.

0
Jason