Bezpośrednie przesyłanie plików do Amazon S3 z przeglądarki klienta - ujawnienie klucza prywatnego


159

Wdrażam bezpośrednie przesyłanie plików z komputera klienckiego do Amazon S3 przez REST API przy użyciu tylko JavaScript, bez żadnego kodu po stronie serwera. Wszystko działa dobrze, ale martwi mnie jedna rzecz ...

Kiedy wysyłam żądanie do Amazon S3 REST API, muszę je podpisać i umieścić podpis w Authenticationnagłówku. Aby utworzyć podpis, muszę użyć mojego tajnego klucza. Ale wszystko dzieje się po stronie klienta, więc tajny klucz można łatwo ujawnić ze źródła strony (nawet jeśli zaciemniam / zaszyfruję moje źródła).

Jak sobie z tym poradzić? I czy to w ogóle problem? Może mogę ograniczyć użycie określonego klucza prywatnego tylko do wywołań REST API z określonego źródła CORS i tylko do metod PUT i POST lub może połączyć klucz tylko z S3 i określonym zasobnikiem? Czy mogą istnieć inne metody uwierzytelniania?

Rozwiązanie „bezserwerowe” jest idealne, ale mogę rozważyć włączenie przetwarzania po stronie serwera, z wyłączeniem przesyłania pliku na mój serwer, a następnie wysyłania go do S3.


7
Bardzo proste: nie przechowuj żadnych sekretów po stronie klienta. Będziesz musiał zaangażować serwer do podpisania żądania.
Ray Nicholus,

1
Przekonasz się również, że podpisywanie i kodowanie base-64 tych żądań jest znacznie łatwiejsze po stronie serwera. Nie wydaje się nierozsądne angażowanie tutaj serwera. Rozumiem, że nie chcę wysyłać wszystkich bajtów pliku na serwer, a następnie do S3, ale podpisywanie żądań po stronie klienta jest bardzo mało korzystne, zwłaszcza, że ​​będzie to nieco trudne i potencjalnie powolne do wykonania po stronie klienta (w javascript).
Ray Nicholus,

5
Jest rok 2016, kiedy architektura bezserwerowa stała się dość popularna, przesyłanie plików bezpośrednio do S3 jest możliwe za pomocą AWS Lambda. Zobacz moją odpowiedź na podobne pytanie: stackoverflow.com/a/40828683/2504317 Zasadniczo miałbyś funkcję Lambda jako adres URL do podpisywania API do przesyłania dla każdego pliku, a twój javascript po stronie clienda po prostu wykonuje HTTP PUT do wstępnie podpisany adres URL. Napisałem komponent Vue, który robi takie rzeczy, kod związany z ładowaniem S3 jest niezależny od biblioteki, spójrz i zrozum.
KF Lin

Inna firma zewnętrzna do przesyłania HTTP / S POST w dowolnym segmencie S3. JS3Upload czysty HTML5: jfileupload.com/products/js3upload-html5/index.html
JFU

Odpowiedzi:


216

Myślę, że to, czego chcesz, to przesyłanie oparte na przeglądarce przy użyciu POST.

Zasadniczo potrzebujesz kodu po stronie serwera, ale wszystko, co robi, to generowanie podpisanych zasad. Gdy kod po stronie klienta ma podpisaną politykę, można go przesłać za pomocą POST bezpośrednio do S3 bez przesyłania danych przez serwer.

Oto oficjalne linki do dokumentów:

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

Przykładowy kod: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

Podpisana polityka zostanie umieszczona w Twoim html w następującej formie:

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

Zauważ, że akcja FORM wysyła plik bezpośrednio do S3 - nie przez twój serwer.

Każdy czas użytkowników chce przesłać plik, należy utworzyć POLICYi SIGNATUREna serwerze. Zwracasz stronę do przeglądarki użytkownika. Użytkownik może następnie przesłać plik bezpośrednio do S3 bez przechodzenia przez serwer.

Po podpisaniu polisy zazwyczaj wygasa się po kilku minutach. Zmusza to użytkowników do komunikowania się z serwerem przed przesłaniem. Pozwala to monitorować i ograniczać wysyłanie, jeśli chcesz.

Jedyne dane przychodzące do lub z serwera to podpisane adresy URL. Twoje tajne klucze pozostają tajne na serwerze.


14
pamiętaj, że używa Signature v2, który wkrótce zostanie zastąpiony przez v4: docs.aws.amazon.com/AmazonS3/latest/API/…
Jörn Berkefeld

9
Upewnij się, że dodałeś ${filename}do nazwy klucza, więc w powyższym przykładzie user/eric/${filename}zamiast tylko user/eric. Jeśli user/ericjest to już istniejący folder, przesyłanie po cichu zakończy się niepowodzeniem (zostaniesz nawet przekierowany do Success_action_redirect), a przesłanych treści tam nie będzie. Po prostu spędziłem godziny na debugowaniu tego, myśląc, że to kwestia uprawnień.
Balint Erdi

@secretmike Jeśli otrzymałeś limit czasu podczas korzystania z tej metody, jak poleciłbyś obejście tego?
Wycieczka

1
@Trip Ponieważ przeglądarka wysyła plik do S3, musisz wykryć przekroczenie czasu w Javascript i samodzielnie zainicjować ponowienie.
Secretmike

@secretmike To pachnie jak nieskończony cykl pętli. Ponieważ limit czasu będzie się powtarzał w nieskończoność dla każdego pliku powyżej 10 / MB.
Wycieczka

40

Możesz to zrobić przez AWS S3 Cognito, wypróbuj ten link tutaj:

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

Wypróbuj również ten kod

Po prostu zmień Region, IdentityPoolId i nazwę swojego zasobnika

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

Aby uzyskać więcej informacji, sprawdź - Github

Czy to obsługuje wiele obrazów?
user2722667

@ user2722667 tak.
Joomler

@Joomler Cześć Dziękuję, ale mam do czynienia z tym problemem w przeglądarce Firefox RequestTimeout Połączenie przez gniazdo z serwerem nie zostało odczytane lub zapisane przed upływem limitu czasu. Bezczynne połączenia zostaną zamknięte, a plik nie zostanie przesłany na S3. Czy możesz mi pomóc, jak mogę rozwiązać ten problem. Dzięki
usama

1
@usama, czy możesz otworzyć sprawę na githubie, ponieważ sprawa nie jest dla mnie
jasna

@Joomler przepraszam za spóźnioną odpowiedź. Otworzyłem problem na GitHub, proszę spojrzeć na to dzięki. github.com/aws/aws-sdk-php/issues/1332
usama

16

Mówisz, że potrzebujesz rozwiązania „bezserwerowego”. Ale to oznacza, że ​​nie masz możliwości umieszczenia żadnego „swojego” kodu w pętli. (UWAGA: gdy już przekażesz swój kod klientowi, jest to teraz „jego” kod). Blokowanie CORS nie pomoże: ludzie mogą łatwo napisać narzędzie inne niż internetowe (lub serwer proxy sieci Web), które dodaje poprawny nagłówek CORS, aby nadużywać systemu.

Duży problem polega na tym, że nie możesz rozróżnić różnych użytkowników. Nie możesz pozwolić jednemu użytkownikowi na wyświetlanie listy / uzyskiwanie dostępu do jego plików, ale uniemożliwić innym to. Jeśli wykryjesz nadużycie, nie możesz nic zrobić poza zmianą klucza. (Które prawdopodobnie napastnik może po prostu odzyskać).

Najlepszym rozwiązaniem jest utworzenie „użytkownika IAM” z kluczem do klienta javascript. Daj mu uprawnienia do zapisu tylko do jednego zasobnika. (ale najlepiej nie włączaj operacji ListBucket, co uczyni ją bardziej atrakcyjną dla atakujących).

Gdybyś miał serwer (nawet prostą mikro-instancję za 20 $ / miesiąc), mógłbyś podpisać klucze na swoim serwerze, monitorując / zapobiegając nadużyciom w czasie rzeczywistym. Bez serwera najlepsze, co możesz zrobić, to okresowe monitorowanie pod kątem nadużyć po fakcie. Oto, co bym zrobił:

1) okresowo zmieniaj klucze tego użytkownika IAM: co noc generuj nowy klucz dla tego użytkownika IAM i zastępuj najstarszy klucz. Ponieważ są 2 klucze, każdy klucz będzie ważny przez 2 dni.

2) włącz rejestrowanie S3 i pobieraj dzienniki co godzinę. Ustaw alerty dotyczące „zbyt wielu przesłanych plików” i „zbyt wielu pobrań”. Będziesz chciał sprawdzić zarówno całkowity rozmiar pliku, jak i liczbę przesłanych plików. Będziesz chciał monitorować zarówno sumy globalne, jak i sumy na adres IP (z niższym progiem).

Te testy można przeprowadzić „bez serwera”, ponieważ można je uruchomić na komputerze stacjonarnym. (tj. S3 wykonuje całą pracę, te procesy tylko po to, aby ostrzec Cię o nadużyciu wiadra S3, abyś nie otrzymał gigantycznego rachunku AWS pod koniec miesiąca).


3
Człowieku, zapomniałem, jak skomplikowane były rzeczy przed Lambdą.
Ryan Shillington

10

Dodając więcej informacji do zaakceptowanej odpowiedzi, możesz odwołać się do mojego bloga, aby zobaczyć działającą wersję kodu, używając AWS Signature w wersji 4.

Podsumuję tutaj:

Gdy tylko użytkownik wybierze plik do przesłania, wykonaj następujące czynności: 1. Zadzwoń do serwera WWW, aby zainicjować usługę w celu wygenerowania wymaganych parametrów

  1. W tej usłudze zadzwoń do usługi AWS IAM, aby uzyskać tymczasowe środki

  2. Po uzyskaniu kredytu utwórz zasadę zasobnika (ciąg zakodowany w oparciu o 64). Następnie podpisz zasady zasobnika tymczasowym tajnym kluczem dostępu, aby wygenerować ostateczny podpis

  3. wyślij niezbędne parametry z powrotem do interfejsu użytkownika

  4. Po otrzymaniu tego, utwórz obiekt formularza html, ustaw wymagane parametry i POST.

Szczegółowe informacje można znaleźć pod adresem https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/


5
Spędziłem cały dzień, próbując rozwiązać ten problem w JavaScript, a ta odpowiedź mówi mi dokładnie, jak to zrobić za pomocą XMLhttprequest. Jestem bardzo zaskoczony, że zostałeś odrzucony. OP poprosił o javascript i otrzymał formularze w zalecanych odpowiedziach. O jeny. Dzięki za tę odpowiedź!
Paul S

BTW superagent ma poważne problemy z CORS, więc xmlhttprequest wydaje się być jedynym rozsądnym sposobem na zrobienie tego teraz
Paul S

4

Aby utworzyć podpis, muszę użyć mojego tajnego klucza. Ale wszystko dzieje się po stronie klienta, więc tajny klucz można łatwo ujawnić ze źródła strony (nawet jeśli zaciemniam / zaszyfruję moje źródła).

To jest, gdy źle zrozumiałeś. Jedynym powodem używania podpisów cyfrowych jest to, że możesz zweryfikować coś jako poprawnego bez ujawniania swojego tajnego klucza. W tym przypadku podpis cyfrowy jest używany, aby uniemożliwić użytkownikowi modyfikowanie zasad ustawionych dla postu formularza.

Podpisy cyfrowe, takie jak ten tutaj, są używane do zapewnienia bezpieczeństwa w całej sieci. Gdyby ktoś (NSA?) Naprawdę był w stanie je złamać, miałby znacznie większe cele niż twoje wiadro S3 :)


2
ale robot może próbować szybko przesłać nieograniczoną liczbę plików. czy mogę ustawić zasadę maksymalnej liczby plików na zasobnik?
Dejell

3

Podałem prosty kod, aby przesłać pliki z przeglądarki Javascript do AWS S3 i wyświetlić listę wszystkich plików w zasobniku S3.

Kroki:

  1. Aby dowiedzieć się, jak utworzyć Create IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Wejdź na stronę konsoli S3 i otwórz konfigurację cors z właściwości zasobnika i napisz w niej następujący kod 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. Utwórz plik HTML zawierający poniższy kod, zmień dane logowania, otwórz plik w przeglądarce i ciesz się.

      <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
Czy nikt nie byłby w stanie użyć mojego identyfikatora „IdentityPoolId” do przesyłania plików do mojego zasobnika S3. W jaki sposób to rozwiązanie zapobiega kopiowaniu mojego identyfikatora „IdentityPoolId” i przesyłaniu wielu plików do mojego zasobnika S3?
Sahil

1
stackoverflow.com/users/4535741/sahil Możesz uniemożliwić przesyłanie danych / plików z innych domen, ustawiając odpowiednie ustawienia CORS na zasobnik S3. Więc nawet jeśli ktoś uzyskał dostęp do twojego identyfikatora puli tożsamości, nie może manipulować twoimi plikami wiadra s3.
Nilesh Pawar

2

Jeśli nie masz żadnego kodu po stronie serwera, twoje bezpieczeństwo zależy od bezpieczeństwa dostępu do twojego kodu JavaScript po stronie klienta (tj. Każdy, kto ma kod, może coś przesłać).

Dlatego zalecałbym po prostu utworzenie specjalnego wiadra S3, który jest publicznie zapisywalny (ale nieczytelny), więc nie potrzebujesz żadnych podpisanych komponentów po stronie klienta.

Nazwa zasobnika (np. GUID) będzie jedyną obroną przed złośliwym przesyłaniem (ale potencjalny napastnik nie może użyć twojego zasobnika do przesyłania danych, ponieważ zapisuje tylko do niego)


1

Oto sposób generowania dokumentu zasad przy użyciu węzłów i serwera bez serwera

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

Używany obiekt konfiguracyjny jest przechowywany w magazynie parametrów SSM i wygląda następująco

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.