클라이언트 브라우저에서 Amazon S3 직접 파일 업로드-개인 키 공개


159

서버 측 코드없이 JavaScript 만 사용하여 REST API를 통해 클라이언트 시스템에서 Amazon S3로 직접 파일 업로드를 구현하고 있습니다. 모든 것이 잘 작동하지만 한 가지 걱정이됩니다 ...

Amazon S3 REST API에 요청을 보낼 때 요청에 서명하고 서명을 Authentication헤더에 넣어야합니다 . 서명을 만들려면 비밀 키를 사용해야합니다. 그러나 모든 것은 클라이언트 측에서 발생하므로 비밀 키는 페이지 소스에서 쉽게 공개 할 수 있습니다 (소스를 난독 화 / 암호화하더라도).

어떻게 처리 할 수 ​​있습니까? 그리고 전혀 문제가 있습니까? 어쩌면 특정 개인 키 사용을 특정 CORS Origin의 REST API 호출 및 PUT 및 POST 메소드로만 제한하거나 키를 S3 및 특정 버킷으로 만 링크 할 수 있습니까? 다른 인증 방법이있을 수 있습니까?

"서버리스"솔루션이 이상적이지만 파일을 서버에 업로드 한 다음 S3로 전송하는 것을 제외하고 일부 서버 측 처리를 고려할 수 있습니다.


7
매우 간단합니다. 클라이언트 측에 비밀을 저장하지 마십시오. 요청에 서명하려면 서버가 필요합니다.
레이 니콜러스

1
또한 이러한 요청을 서명하고 base-64로 인코딩하는 것이 서버 측에서 훨씬 쉽다는 것을 알 수 있습니다. 여기에 서버를 포함시키는 것은 불합리한 것처럼 보이지 않습니다. 모든 파일 바이트를 서버로 보낸 다음 S3까지 보내고 싶지 않다는 것을 이해할 수 있지만 클라이언트 측 요청에 서명하면 이점이 거의 없습니다. 특히 클라이언트 측에서는 약간 어려우며 잠재적으로 느릴 수 있기 때문입니다. (자바 스크립트에서).
레이 니콜러스

5
2016 년에 서버리스 아키텍처가 널리 보급되면서 AWS Lambda의 도움으로 S3에 파일을 직접 업로드 할 수 있습니다 . 비슷한 질문에 대한 내 대답을보십시오 : stackoverflow.com/a/40828683/2504317 기본적으로 각 파일에 대해 업로드 가능한 URL을 서명하는 API로 Lambda 함수가 있으며 cliend-side 자바 스크립트는 HTTP PUT을 수행합니다. 사전 서명 된 URL. 나는 그런 일을하는 Vue 구성 요소를 썼다 .S3 업로드 관련 코드 는 라이브러리에 구애받지 않고 살펴보고 아이디어를 얻는다.
KF Lin

S3 버킷에서 HTTP / S POST 업로드를위한 다른 타사. JS3Upload 순수 HTML5 : jfileupload.com/products/js3upload-html5/index.html
JFU

답변:


216

POST를 사용하는 브라우저 기반 업로드가 원하는 것 같습니다.

기본적으로 서버 측 코드가 필요하지만 서명 된 정책 만 생성하면됩니다. 클라이언트 측 코드에 서명 된 정책이 있으면 데이터가 서버를 통하지 않고 POST를 사용하여 S3에 직접 업로드 할 수 있습니다.

공식 문서 링크는 다음과 같습니다.

다이어그램 : http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html

예제 코드 : http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

서명 된 정책은 다음과 같은 형식으로 html로 작성됩니다.

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

FORM 조치는 파일 서버가 아닌 S3으로 직접 전송 합니다.

사용자 중 한 명이 파일을 업로드하려고 할 때마다 POLICYSIGNATURE서버에 파일을 작성 합니다. 페이지를 사용자 브라우저로 되돌립니다. 그런 다음 사용자는 서버를 통하지 않고 파일을 S3에 직접 업로드 할 수 있습니다.

정책에 서명하면 일반적으로 몇 분 후에 정책이 만료됩니다. 이렇게하면 사용자가 업로드하기 전에 서버와 대화해야합니다. 원하는 경우 업로드를 모니터링하고 제한 할 수 있습니다.

서버와주고받는 유일한 데이터는 서명 된 URL입니다. 비밀 키는 서버에서 비밀로 유지됩니다.


14
주의하시기 바랍니다 곧 V4로 대체됩니다이 사용하는 서명 V2 : docs.aws.amazon.com/AmazonS3/latest/API/...
요른 Berkefeld

9
${filename}위의 예에서는 키 user/eric/${filename}대신 키 이름 을 추가 해야합니다 user/eric. 경우 user/eric이미 존재하는 폴더, 업로드는 자동으로 실패합니다 (당신도 success_action_redirect로 리디렉션됩니다)하고, 업로드 된 내용이 없을 것입니다. 이 문제를 디버깅하는 데 시간이 걸렸으며 이는 권한 문제였습니다.
Balint Erdi 2016 년

@secretmike이 방법을 사용하여 시간 초과가 발생한 경우이를 우회하는 것이 좋습니다.
여행

1
@Trip 브라우저가 파일을 S3로 전송하므로 Javascript에서 시간 초과를 감지하고 다시 시도해야합니다.
secretmike

@secretmike 그것은 무한 루프 사이클 냄새가납니다. 시간 초과는 10 / mbs를 초과하는 파일에 대해 무한정 반복됩니다.
여행

40

AWS S3 Cognito가이 링크를 시도하면됩니다.

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

이 코드를 사용해보십시오

리전, IdentityPoolId 및 버킷 이름 만 변경하면됩니다.

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

자세한 내용은 Github을 확인하십시오.

여러 이미지를 지원합니까?
user2722667

@ user2722667 그렇습니다.
Joomler

@Joomler 안녕하세요 감사하지만 firefox 에서이 문제에 직면하고 있습니다. RequestTimeout 서버에 대한 소켓 연결이 시간 초과 기간 내에 읽히지 않았거나 쓰여지지 않았습니다. 유휴 연결이 닫히고 파일이 S3에 업로드되지 않습니다.이 문제를 해결하는 방법을 알려주세요. 감사합니다
usama

1
@usama 문제가 명확하지 않기 때문에 github에서 문제를 열 ​​수 있습니까
Joomler

@Joomler 늦게 답장을 보내서 죄송합니다 .GitHub에서 문제를 열었습니다.이 감사를 살펴보십시오. github.com/aws/aws-sdk-php/issues/1332
usama

16

"서버리스"솔루션을 원한다고 말하고 있습니다. 그러나 이는 "귀하의"코드를 루프에 넣을 수있는 능력이 없다는 것을 의미합니다. (참고 : 일단 코드를 클라이언트에 제공하면 코드는 "그들의"코드입니다.) CORS를 잠그면 도움이되지 않습니다. 사람들은 웹 기반이 아닌 도구 (또는 웹 기반 프록시)를 쉽게 작성할 수 있습니다 올바른 CORS 헤더가 시스템을 남용합니다.

가장 큰 문제는 다른 사용자를 구별 할 수 없다는 것입니다. 한 사용자가 자신의 파일을 나열 / 액세스하도록 허용 할 수는 없지만 다른 사용자가 파일을 액세스하지 못하게합니다. 악용을 감지하면 키 변경을 제외하고는 할 수있는 일이 없습니다. (공격자는 아마도 다시 도착할 수 있습니다.)

최선의 방법은 자바 스크립트 클라이언트 용 키를 사용하여 "IAM 사용자"를 만드는 것입니다. 하나의 버킷에만 쓰기 권한을 부여하십시오. (그러나 이상적으로는 ListBucket 작업을 활성화하지 마십시오. 공격자에게 더 매력적입니다.)

서버가있는 경우 (월간 $ 20의 간단한 마이크로 인스턴스도) 실시간으로 남용을 모니터링 / 예방하면서 서버의 키에 서명 할 수 있습니다. 서버가 없으면 최선을 다해 사후 악용 사례를 정기적으로 모니터링하는 것이 가장 좋습니다. 내가 할 일은 다음과 같습니다.

1) 해당 IAM 사용자의 키를 주기적으로 교체합니다. 매일 밤 해당 IAM 사용자의 새 키를 생성하고 가장 오래된 키를 바꿉니다. 2 개의 키가 있으므로 각 키는 2 일 동안 유효합니다.

2) S3 로깅을 활성화하고 매시간 로그를 다운로드하십시오. "너무 많은 업로드"및 "너무 많은 다운로드"에 대해 경고를 설정하십시오. 총 파일 크기와 업로드 된 파일 수를 모두 확인하려고합니다. 또한 전체 총계와 IP 주소 당 총계 (임계 값이 낮은)를 모두 모니터링하려고합니다.

이러한 검사는 데스크탑에서 실행할 수 있으므로 "서버리스"로 수행 할 수 있습니다. (즉, S3는 모든 작업을 수행합니다. 이러한 프로세스는 S3 버킷 남용을 경고하기 위해 해당 월말에 대규모 AWS 요금을 받지 않습니다 .)


3
람다 이전에 얼마나 복잡한 일이 있었는지 잊어 버렸습니다.
Ryan Shillington

10

허용 된 답변에 더 많은 정보를 추가하면 AWS 서명 버전 4를 사용하여 실행중인 코드 버전을 확인하기 위해 내 블로그를 참조 할 수 있습니다.

여기에 요약됩니다 :

사용자가 업로드 할 파일을 선택하자마자 다음을 수행하십시오. 1. 웹 서버를 호출하여 필요한 매개 변수를 생성하는 서비스를 시작하십시오.

  1. 이 서비스에서 AWS IAM 서비스를 호출하여 임시 자격 증명을 받으십시오.

  2. cred가 있으면 버킷 정책 (기본 64 인코딩 문자열)을 생성하십시오. 그런 다음 임시 비밀 액세스 키로 버킷 정책에 서명하여 최종 서명을 생성하십시오.

  3. 필요한 매개 변수를 UI로 다시 전송

  4. 이것이 수신되면 html 양식 오브젝트를 작성하고 필수 매개 변수를 설정 한 후 POST하십시오.

자세한 내용은 https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/ 를 참조 하십시오.


5
나는 하루 종일 Javascript로 이것을 알아 내려고 노력 했으며이 답변은 XMLhttprequest를 사용하여이를 수행하는 방법을 정확하게 알려줍니다. 난 당신이 downvoted있어 매우 놀랍습니다. OP는 자바 스크립트를 요청했으며 권장 답변에 양식을 얻었습니다. 맙소사. 이 답변에 감사드립니다!
Paul S

BTW 슈퍼 에이전트가 심각한 CORS 문제를 가지고 있으므로 XMLHttpRequest를 지금이 권리를 할 수있는 유일한 합리적인 방법 전자 보인다
폴 S

4

서명을 만들려면 비밀 키를 사용해야합니다. 그러나 모든 것은 클라이언트 측에서 발생하므로 비밀 키는 페이지 소스에서 쉽게 공개 할 수 있습니다 (소스를 난독 화 / 암호화하더라도).

이곳은 당신이 오해 한 곳입니다. 디지털 서명을 사용하는 이유는 비밀 키를 밝히지 않고 올바른 것을 확인할 수 있기 때문입니다. 이 경우 사용자가 양식 게시에 설정 한 정책을 수정하지 못하도록 디지털 서명이 사용됩니다.

여기에있는 것과 같은 디지털 서명은 웹 전체의 보안에 사용됩니다. 누군가 (NSA?)가 실제로 그들을 깨뜨릴 수 있다면 S3 버킷보다 훨씬 큰 목표를 가질 것입니다 :)


2
그러나 로봇은 무제한 파일을 빠르게 업로드하려고 시도 할 수 있습니다. 버킷 당 최대 파일 정책을 설정할 수 있습니까?
Dejell

3

Javascript 브라우저에서 AWS S3로 파일을 업로드하고 S3 버킷의 모든 파일을 나열하는 간단한 코드를 제공했습니다.

단계 :

  1. IdentityPoolId 생성 방법을 이해하려면 http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. S3의 콘솔 페이지로 이동하여 버킷 속성에서 cors 구성을 열고 다음 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. 다음 코드가 포함 된 HTML 파일을 만들어 자격 증명을 변경하고 브라우저에서 파일을 열고 즐기십시오.

      <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
아무도 내 S3 버킷에 파일을 업로드하기 위해 "IdentityPoolId"를 사용할 수 없습니다. 이 솔루션은 타사가 내 "IdentityPoolId"를 복사하고 S3 버킷에 많은 파일을 업로드하는 것을 어떻게 방지합니까?
Sahil

1
stackoverflow.com/users/4535741/sahil 적절한 CORS 설정을 S3 버킷으로 설정하여 다른 도메인에서 데이터 / 파일 업로드를 방지 할 수 있습니다. 따라서 사용자가 자격 증명 풀 ID에 액세스 한 경우에도 s3 버킷 파일을 조작 할 수 없습니다.
Nilesh Pawar

2

서버 측 코드가없는 경우 보안은 클라이언트 측에서 JavaScript 코드에 대한 액세스 보안에 의존합니다 (예 : 코드를 가진 모든 사용자가 무언가를 업로드 할 수 있음).

따라서 공개 쓰기 가능하지만 읽을 수없는 특수 S3 버킷을 만드는 것이 좋습니다. 따라서 클라이언트 측에 서명 된 구성 요소가 필요하지 않습니다.

버킷 이름 (예 : GUID)은 악의적 인 업로드에 대한 유일한 방어 수단이지만 잠재적 인 공격자는 버킷 만 쓸 수 있기 때문에 버킷을 사용하여 데이터를 전송할 수 없습니다.


1

다음은 노드 및 서버리스를 사용하여 정책 문서를 생성하는 방법입니다

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

사용 된 구성 객체는 SSM Parameter Store에 저장 되며 다음과 같습니다.

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

0

타사 서비스를 사용하려는 경우 auth0.com은이 통합을 지원합니다. auth0 서비스는 타사 SSO 서비스 인증을 AWS 임시 세션 토큰으로 교환하여 권한이 제한됩니다.

https://github.com/auth0-samples/auth0-s3-sample/
및 auth0 설명서를 참조하십시오 .


1
내가 이해하는 것처럼-이제 우리는 그것을 위해 Cognito를 가지고 있습니까?
Vitaly Zdanevich
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.