ti-enxame.com

Upload de arquivo direto do Amazon S3 a partir do navegador do cliente - divulgação de chave privada

Estou implementando um upload direto de arquivo da máquina cliente para o Amazon S3 por meio da API REST usando apenas JavaScript, sem nenhum código do lado do servidor. Tudo funciona bem, mas uma coisa está me preocupando ...

Quando envio uma solicitação para a API do Amazon S3 REST, preciso assinar a solicitação e colocar uma assinatura no cabeçalho Authentication. Para criar uma assinatura, devo usar minha chave secreta. Mas todas as coisas acontecem do lado do cliente, portanto, a chave secreta pode ser facilmente revelada a partir da origem da página (mesmo que eu ofusque/criptografe minhas fontes).

Como posso lidar com isso? E isso é um problema? Talvez eu possa limitar o uso de chave privada específica apenas para chamadas de API REST de uma origem de CORS específica e somente para métodos PUT e POST ou talvez uma chave de link para somente S3 e bucket específico? Pode haver outros métodos de autenticação?

A solução "sem servidor" é ideal, mas posso considerar o envolvimento de algum processamento no lado do servidor, excluindo o upload de um arquivo para o meu servidor e, em seguida, enviar para o S3.

132
Olegas

Eu acho que o que você quer é Uploads baseados em navegador usando POST.

Basicamente, você precisa de código do lado do servidor, mas tudo o que ele faz é gerar políticas assinadas. Quando o código do lado do cliente tiver a política assinada, ele poderá fazer o upload usando POST diretamente para o S3 sem que os dados passem pelo servidor.

Aqui estão os links oficiais do doc: 

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

Exemplo de código: http://docs.aws.Amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

A política assinada entraria no seu html de uma forma como esta:

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

Observe que a ação FORM está enviando o arquivo diretamente para o S3 - não através do seu servidor.

Toda vez que um de seus usuários deseja fazer upload de um arquivo, você criaria a POLICY e SIGNATURE em seu servidor. Você retorna a página ao navegador do usuário. O usuário pode então carregar um arquivo diretamente para o S3 sem passar pelo seu servidor.

Quando você assina a política, normalmente a política expira após alguns minutos. Isso força seus usuários a falar com seu servidor antes de fazer o upload. Isso permite monitorar e limitar os uploads, se desejar.

Os únicos dados que vão para ou do seu servidor são os URLs assinados. Suas chaves secretas permanecem em segredo no servidor.

195
secretmike

Você está dizendo que quer uma solução "sem servidor". Mas isso significa que você não tem capacidade de colocar qualquer código "seu" no loop. (NOTA: Uma vez que você forneça seu código a um cliente, é o código "deles" agora.) O bloqueio do CORS não ajudará: as pessoas podem escrever facilmente uma ferramenta não baseada na Web (ou um proxy baseado na Web) que adiciona o cabeçalho CORS correto para abusar do seu sistema.

O grande problema é que você não pode diferenciar entre os diferentes usuários. Você não pode permitir que um usuário liste/acesse seus arquivos, mas impede que outros o façam. Se você detectar abuso, não há nada que você possa fazer a não ser alterar a chave. (Que o atacante pode presumivelmente pegar de novo.)

Sua melhor aposta é criar um "usuário do IAM" com uma chave para o seu cliente javascript. Apenas dê acesso de gravação a apenas um balde. (mas, idealmente, não habilite a operação ListBucket, o que tornará mais atraente para os invasores.)

Se você tivesse um servidor (até mesmo uma micro instância simples de US $ 20/mês), você poderia assinar as chaves em seu servidor enquanto monitorava/prevenia o abuso em tempo real. Sem um servidor, o melhor que você pode fazer é monitorar periodicamente o abuso após o fato. Aqui está o que eu faria:

1) rotacione periodicamente as chaves para esse usuário do IAM: Todas as noites, gere uma nova chave para esse usuário do IAM e substitua a chave mais antiga. Como há duas chaves, cada chave será válida por dois dias.

2) habilite o log S3 e baixe os logs a cada hora. Definir alertas em "muitos uploads" e "muitos downloads". Você desejará verificar o tamanho total do arquivo e o número de arquivos enviados. E você desejará monitorar os totais globais e também os totais de endereços IP (com um limite mais baixo).

Essas verificações podem ser feitas "sem servidor" porque você pode executá-las na sua área de trabalho. (isto é, o S3 faz todo o trabalho, esses processos apenas para alertá-lo sobre o abuso do seu bucket do S3 para que você não receba uma fatura gigante AWS no final do mês.)

15
BraveNewCurrency

Adicionando mais informações à resposta aceita, você pode consultar meu blog para ver uma versão em execução do código, usando o AWS Signature versão 4.

Resumiremos aqui:

Assim que o usuário selecionar um arquivo para ser carregado, faça o seguinte: 1. Faça uma chamada para o servidor da web para iniciar um serviço para gerar parâmetros obrigatórios

  1. Neste serviço, faça uma chamada para o serviço AWS IAM para obter credibilidade temporária

  2. Depois de ter credito, crie uma política de bucket (string codificada na base 64). Em seguida, assine a política de buckets com a chave de acesso secreto temporário para gerar a assinatura final

  3. enviar os parâmetros necessários de volta para a interface do usuário

  4. Quando isso for recebido, crie um objeto de formulário html, defina os parâmetros exigidos e POST.

Para informações detalhadas, por favor, consulte https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

8
RajeevJ

Para criar uma assinatura, devo usar minha chave secreta. Mas todas as coisas Acontecem do lado do cliente, portanto, a chave secreta pode ser facilmente revelada Do código-fonte da página (mesmo que eu ofusque/criptografe minhas fontes).

É aqui que você entendeu mal. A razão pela qual as assinaturas digitais são usadas é para que você possa verificar algo como correto sem revelar sua chave secreta. Nesse caso, a assinatura digital é usada para impedir que o usuário modifique a política que você definiu para a postagem do formulário.

Assinaturas digitais como a aqui são usadas para segurança em toda a web. Se alguém (NSA?) Realmente conseguisse quebrá-los, eles teriam alvos muito maiores que o seu S3 :)

4
OlliM

Se você não tem nenhum código do lado do servidor, sua segurança depende da segurança do acesso ao seu código JavaScript no lado do cliente (ou seja, todos que possuem o código podem fazer upload de algo).

Então, eu recomendaria, para simplesmente criar um bucket S3 especial que é public gravável (mas não legível), assim você não precisa de nenhum componente assinado no lado do cliente.

O nome do intervalo (a GUID, por exemplo) será sua única defesa contra envios mal-intencionados (mas um invasor em potencial não pode usar seu bloco para transferir dados, porque ele é somente para ele)

2
Ruediger Jungbeck

Eu dei um código simples para fazer upload de arquivos do navegador Javascript para o AWS S3 e listar todos os arquivos no bucket do S3.

Passos:

  1. Para saber como criar o Create IdentityPoolId http://docs.aws.Amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Vá para a página de console do S3 e abra a configuração de cors a partir das propriedades do bucket e escreva o código XML a seguir.

      <?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. Crie o arquivo de HTML que contém código seguinte mude as credenciais, abra arquivo no navegador e desfrute.

      <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

Aqui está como você gera um documento de política usando o nó e sem servidor

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

O objeto de configuração utilizado é armazenado em SSM Parameter Store e é semelhante a este

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

Se você estiver disposto a usar um serviço de terceiros, o auth0.com suporta essa integração. O serviço auth0 troca uma autenticação de serviço de SSO de terceiros para que um token de sessão temporária da AWS tenha permissões limitadas.

Veja: https://github.com/auth0-samples/auth0-s3-sample/
e a documentação auth0.

0
Jason