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


159

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

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

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

“无服务器”解决方案是理想的选择,但是我可以考虑进行一些服务器端处理,而不是将文件上传到服务器,然后再发送到S3。


7
很简单:不要在客户端存储任何秘密。您将需要让服务器签署请求。
Ray Nicholus 2013年

1
您还将发现对这些请求进行签名和base-64编码更容易在服务器端进行。在这里使用一台服务器似乎并非没有道理。我可以理解,不希望将所有文件字节都发送到服务器,然后再发送到S3,但是在客户端签署请求几乎没有什么好处,尤其是因为这样做有点挑战性,并且客户端执行起来可能很慢(在javascript中)。
Ray Nicholus

5
在2016年,随着无服务器架构变得非常流行,借助AWS Lambda可以将文件直接上传 到S3。请参阅我对类似问题的回答:stackoverflow.com/a/40828683/2504317基本上,您会使用Lambda函数作为每个文件的API签名可上传网址,并且您的客户端javascript只需对预签名的URL。我已经编写了一个Vue组件来执行此类操作,与S3上传相关的代码与库无关,请看看并了解其想法。
KF Lin

在任何S3存储桶中用于HTTP / S POST上传的另一个第三方。JS3Upload纯HTML5:jfileupload.com/products/js3upload-html5/index.html
JFU

Answers:


215

我认为您想要的是使用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
请注意,这里使用签名V2将很快被V4替换:docs.aws.amazon.com/AmazonS3/latest/API/...
乔恩Berkefeld

9
确保确保添加${filename}到键名,因此对于上面的示例,user/eric/${filename}不要仅仅添加user/eric。如果user/eric已经存在一个文件夹,则上传将静默失败(您甚至将被重定向到success_action_redirect),并且上传的内容将不存在。只是花了几个小时调试这种想法,这是一个权限问题。
Balint Erdi 2015年

@secretmike如果执行此方法收到超时,您将如何建议绕过它?
2016年

1
@旅行由于浏览器正在将文件发送到S3,因此您需要检测Java脚本中的超时并自行发起重试。
secretmike '16

@secretmike闻起来像一个无限循环。对于超过10 / mbs的任何文件,超时都会无限期地重复。
2016年

40

您可以通过AWS S3 Cognito做到这一点,在这里尝试以下链接:

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

也试试这个代码

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

有关更多详细信息,请检查-Github

这是否支持多张图片?
user2722667

@ user2722667是的。
Joomler '17

@Joomler嗨,谢谢,但是我在firefox RequestTimeout上遇到此问题。在超时时间内未读取或写入服务器的套接字连接。空闲连接将关闭,并且文件不会在S3.Up上上传。能帮助我如何解决此问题
谢谢

1
@usama能否请您在github中打开此问题,因为这个问题对我而言尚不明确
Joomler '17

@Joomler很抱歉在这里延迟回复我已经在GitHub上打开了一个问题,请查看此谢谢。github.com/aws/aws-sdk-php/issues/1332
usama

16

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

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

最好的选择是为您的JavaScript客户端创建一个带有密钥的“ IAM用户”。仅授予它对一个存储桶的写入权限。(但理想情况下,请勿启用ListBucket操作,这会使攻击者更具吸引力。)

如果您有一台服务器(甚至是一个简单的微型实例,每月需支付20美元),则可以在服务器上对密钥进行签名,同时实时监控/防止滥用。如果没有服务器,您可以做的最好的事情就是定期监视事后情况。这是我会做的:

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

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

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


3
伙计,我忘记了Lambda之前的事情有多复杂。
瑞安·希灵顿

10

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

将在这里总结:

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

  1. 在此服务中,致电AWS IAM服务以获取临时信用

  2. 有了资格后,请创建一个存储桶策略(基于64位编码的字符串)。然后使用临时秘密访问密钥对存储桶策略进行签名以生成最终签名

  3. 将必要的参数发送回用户界面

  4. 接收到此信息后,创建一个html表单对象,设置所需的参数并对其进行发布。

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


5
我花了整整一天的时间尝试用Javascript弄清楚这一点,这个答案准确地告诉了我如何使用XMLhttprequest做到这一点。我很惊讶你被否决了。OP要求使用javascript并获得建议答案中的表格。真是的 感谢您的回答!
Paul S

BTW的SuperAgent有严重CORS问题,所以XMLHttpRequest的似乎以电子邮件的唯一合理的方法现在这样做的权利
保罗小号

4

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

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

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


2
但是机器人可能会尝试快速上传无限制的文件。可以设置每个存储区最大文件数的策略吗?
Dejell '16

3

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

脚步:

  1. 知道如何创建Create 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
没有人能够使用我的“ IdentityPoolId”将文件上传到我的S3存储桶。该解决方案如何防止任何第三方仅复制我的“ IdentityPoolId”并将大量文件上传到我的S3存储桶?
萨希尔

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 参数存储中,如下所示

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.