javascript 从客户端浏览器直接上传 Amazon S3 文件 - 私钥泄露

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17585881/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-27 08:53:41  来源:igfitidea点击:

Amazon S3 direct file upload from client browser - private key disclosure

javascriptamazon-web-servicesauthenticationamazon-s3

提问by Olegas

I'm implementing a direct file upload from client machine to Amazon S3 via REST API using only JavaScript, without any server-side code. All works fine but one thing is worrying me...

我仅使用 JavaScript 通过 REST API 实现从客户端计算机到 Amazon S3 的直接文件上传,无需任何服务器端代码。一切正常,但有一件事让我担心......

When I send a request to Amazon S3 REST API, I need to sign the request and put a signature into Authenticationheader. To create a signature, I must use my secret key. But all things happens on a client side, so, the secret key can be easily revealed from page source (even if I obfuscate/encrypt my sources).

当我向 Amazon S3 REST API 发送请求时,我需要对请求进行签名并将签名放入Authentication标头中。要创建签名,我必须使用我的秘密密钥。但是所有事情都发生在客户端,因此,可以轻松地从页面源中泄露密钥(即使我混淆/加密了我的源)。

How can I handle this? And is it a problem at all? Maybe I can limit specific private key usage only to REST API calls from a specific CORS Origin and to only PUT and POST methods or maybe link key to only S3 and specific bucket? May be there are another authentication methods?

我该如何处理?这真的是个问题吗?也许我可以将特定私钥的使用限制为仅来自特定 CORS 源的 REST API 调用以及仅 PUT 和 POST 方法,或者仅将链接密钥链接到 S3 和特定存储桶?可能还有其他身份验证方法吗?

"Serverless" solution is ideal, but I can consider involving some serverside processing, excluding uploading a file to my server and then send in to S3.

“无服务器”解决方案是理想的,但我可以考虑涉及一些服务器端处理,不包括将文件上传到我的服务器然后发送到 S3。

回答by secretmike

I think what you want is Browser-Based Uploads Using POST.

我认为您想要的是使用 POST 的基于浏览器的上传。

Basically, you do need server-side code, but all it does is generate signed policies. Once the client-side code has the signed policy, it can upload using POST directly to S3 without the data going through your server.

基本上,您确实需要服务器端代码,但它所做的只是生成已签名的策略。一旦客户端代码具有签名策略,它就可以使用 POST 直接上传到 S3,而无需数据通过您的服务器。

Here's the official doc links:

这是官方文档链接:

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

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

Example code: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

示例代码:http: //docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

The signed policy would go in your html in a form like this:

签署的政策将以如下形式出现在您的 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>

Notice the FORM action is sending the file directly to S3- not via your server.

请注意 FORM 操作是将文件直接发送到 S3- 而不是通过您的服务器。

Every time one of your users wants to upload a file, you would create the POLICYand SIGNATUREon your server. You return the page to the user's browser. The user can then upload a file directly to S3 without going through your server.

每次您的用户想要上传文件时,您都会在服务器上创建POLICYSIGNATURE。您将页面返回到用户的浏览器。然后,用户可以将文件直接上传到 S3,而无需通过您的服务器。

When you sign the policy, you typically make the policy expire after a few minutes. This forces your users to talk to your server before uploading. This lets you monitor and limit uploads if you desire.

签署策略时,您通常会使策略在几分钟后过期。这会强制您的用户在上传之前与您的服务器通话。这使您可以根据需要监控和限制上传。

The only data going to or from your server is the signed URLs. Your secret keys stay secret on the server.

传入或传出服务器的唯一数据是签名 URL。您的密钥在服务器上保密。

回答by Joomler

You can do this by AWS S3 Cognito try this link here :

您可以通过 AWS S3 Cognito 执行此操作,请在此处尝试此链接:

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

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

Also try this code

也试试这个代码

Just change Region, IdentityPoolId and Your bucket name

只需更改 Region、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>

欲知更多详情,请查看—— GithubGitHub

回答by BraveNewCurrency

You're saying you want a "serverless" solution. But that means you have no ability to put any of "your" code in the loop. (NOTE: Once you give your code to a client, it's "their" code now.) Locking down CORS is not going to help: People can easily write a non-web-based tool (or a web-based proxy) that adds the correct CORS header to abuse your system.

您是说您想要一个“无服务器”解决方案。但这意味着您无法将任何“您的”代码放入循环中。(注意:一旦您将代码提供给客户,它现在就是“他们的”代码。)锁定 CORS 无济于事:人们可以轻松编写一个非基于 Web 的工具(或基于 Web 的代理)来添加正确的 CORS 标头滥用您的系统。

The big problem is that you can't differentiate between the different users. You can't allow one user to list/access his files, but prevent others from doing so. If you detect abuse, there is nothing you can do about it except change the key. (Which the attacker can presumably just get again.)

最大的问题是您无法区分不同的用户。你不能允许一个用户列出/访问他的文件,但阻止其他人这样做。如果您发现滥用行为,除了更改密钥外,您无能为力。(攻击者大概可以再次获得。)

Your best bet is to create an "IAM user" with a key for your javascript client. Only give it write access to just one bucket. (but ideally, do not enable the ListBucket operation, that will make it more attractive to attackers.)

最好的办法是使用 JavaScript 客户端的密钥创建“IAM 用户”。只授予它对一个存储桶的写访问权限。(但理想情况下,不要启用 ListBucket 操作,这会使其对攻击者更具吸引力。)

If you had a server (even a simple micro instance at $20/month), you could sign the keys on your server while monitoring/preventing abuse in realtime. Without a server, the best you can do is periodically monitor for abuse after-the-fact. Here's what I would do:

如果您有一台服务器(即使是一个简单的微型实例,每月 20 美元),您就可以在服务器上签署密钥,同时实时监控/防止滥用。如果没有服务器,您能做的最好的事情就是在事后定期监控滥用情况。这是我会做的:

1) periodically rotate the keys for that IAM user: Every night, generate a new key for that IAM user, and replace the oldest key. Since there are 2 keys, each key will be valid for 2 days.

1) 定期轮换该 IAM 用户的密钥:每天晚上,为该 IAM 用户生成一个新密钥,并替换最旧的密钥。由于有 2 个密钥,每个密钥的有效期为 2 天。

2) enable S3 logging, and download the logs every hour. Set alerts on "too many uploads" and "too many downloads". You will want to check both total file size and number of files uploaded. And you will want to monitor both the global totals, and also the per-IP address totals (with a lower threshold).

2)开启S3日志,每小时下载一次日志。设置“上传过多”和“下载过多”的警报。您需要检查总文件大小和上传的文件数量。并且您将需要监视全局总数和每个 IP 地址的总数(具有较低的阈值)。

These checks can be done "serverless" because you can run them on your desktop. (i.e. S3 does all the work, these processes just there to alert you to abuse of your S3 bucket so you don't get a giantAWS bill at the end of the month.)

这些检查可以“无服务器”完成,因为您可以在桌面上运行它们。(即S3完成所有的工作,这些过程就在那里,提醒您滥用您的S3存储的,这样你就不会得到一个巨人在月底AWS账单。)

回答by RajeevJ

Adding more info to the accepted answer, you can refer to my blog to see a running version of the code, using AWS Signature version 4.

在接受的答案中添加更多信息,您可以参考我的博客以查看代码的运行版本,使用 AWS 签名版本 4。

Will summarize here:

将在这里总结:

As soon as the user selects a file to be uploaded, do the followings: 1. Make a call to the web server to initiate a service to generate required params

一旦用户选择了要上传的文件,请执行以下操作: 1. 调用 Web 服务器以启动服务以生成所需的参数

  1. In this service, make a call to AWS IAM service to get temporary cred

  2. Once you have the cred, create a bucket policy (base 64 encoded string). Then sign the bucket policy with the temporary secret access key to generate final signature

  3. send the necessary parameters back to the UI

  4. Once this is received, create a html form object, set the required params and POST it.

  1. 在此服务中,调用 AWS IAM 服务以获取临时凭证

  2. 获得信用后,创建一个存储桶策略(base 64 编码字符串)。然后用临时秘密访问密钥签署桶策略以生成最终签名

  3. 将必要的参数发送回 UI

  4. 收到后,创建一个 html 表单对象,设置所需的参数并发布它。

For detailed info, please refer https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

有关详细信息,请参阅 https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

回答by OlliM

To create a signature, I must use my secret key. But all things happens on a client side, so, the secret key can be easily revealed from page source (even if I obfuscate/encrypt my sources).

要创建签名,我必须使用我的秘密密钥。但是所有事情都发生在客户端,因此,可以轻松地从页面源中泄露密钥(即使我混淆/加密了我的源)。

This is where you have misunderstood. The very reason digital signatures are used is so that you can verify something as correct without revealing your secret key. In this case the digital signature is used to prevent the user from modifying the policy you set for the form post.

这就是你误会的地方。使用数字签名的真正原因是,您可以在不泄露密钥的情况下验证某些内容是否正确。在这种情况下,数字签名用于防止用户修改您为表单发布设置的策略。

Digital signatures such as the one here are used for security all around the web. If someone (NSA?) really were able to break them, they would have much bigger targets than your S3 bucket :)

诸如此处的数字签名用于整个网络的安全性。如果有人(NSA?)真的能够破解它们,他们的目标将比您的 S3 存储桶大得多 :)

回答by Nilesh Pawar

I have given a simple code to upload files from Javascript browser to AWS S3 and list the all files in S3 bucket.

我给出了一个简单的代码来将文件从 Javascript 浏览器上传到 AWS S3 并列出 S3 存储桶中的所有文件。

Steps:

脚步:

  1. To know how to create Create IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Goto S3's console page and open cors configuration from bucket properties and write following XML code into that.

      <?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. Create HTML file containing following code change the credentials, open file in browser and enjoy.

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

回答by Ruediger Jungbeck

If you don't have any server side code, you security depends on the security of the access to your JavaScript code on the client side (ie everybody who has the code could upload something).

如果您没有任何服务器端代码,那么您的安全性取决于在客户端访问您的 JavaScript 代码的安全性(即每个拥有代码的人都可以上传一些东西)。

So I would recommend, to simply create a special S3 bucket which is public writeable (but not readable), so you don't need any signed components on the client side.

所以我建议,简单地创建一个特殊的 S3 存储桶,它是公共可写的(但不可读),这样你就不需要客户端的任何签名组件。

The bucket name (a GUID eg) will be your only defense against malicious uploads (but a potential attacker could not use your bucket to transfer data, because it is write only to him)

存储桶名称(例如 GUID)将是您抵御恶意上传的唯一防御措施(但潜在的攻击者无法使用您的存储桶传输数据,因为它只写给他)

回答by Samir Patel

Here is how you generate a policy document using node and 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;

The configuration object used is stored in SSM Parameter Storeand looks like this

使用的配置对象存储在 SSM Parameter Store 中,看起来像这样

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

回答by Jason

If you are willing to use a 3rd party service, auth0.com supports this integration. The auth0 service exchanges a 3rd party SSO service authentication for an AWS temporary session token will limited permissions.

如果您愿意使用第三方服务,auth0.com 支持此集成。auth0 服务为 AWS 临时会话令牌交换第 3 方 SSO 服务身份验证将限制权限。

See: https://github.com/auth0-samples/auth0-s3-sample/
and the auth0 documentation.

请参阅:https: //github.com/auth0-samples/auth0-s3-sample/
和 auth0 文档。