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

Posted

技术标签:

【中文标题】从客户端浏览器直接上传 Amazon S3 文件 - 私钥泄露【英文标题】:Amazon S3 direct file upload from client browser - private key disclosure 【发布时间】:2013-07-09 06:46:41 【问题描述】:

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

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

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

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

【问题讨论】:

非常简单:不要在客户端存储任何秘密。您需要让服务器对请求进行签名。 您还会发现在服务器端对这些请求进行签名和base-64 编码要容易得多。在这里涉及服务器似乎并非不合理。我可以理解不想将所有文件字节发送到服务器然后再发送到 S3,但是在客户端签署请求几乎没有什么好处,特别是因为这将有点挑战并且在客户端执行时可能会很慢(在 javascript 中)。 2016 年,随着无服务器架构变得非常流行,在 AWS Lambda 的帮助下,将文件直接上传到 S3 成为可能。请参阅我对类似问题的回答:***.com/a/40828683/2504317 基本上,您将拥有一个 Lambda 函数作为每个文件的 API 签名可上传 URL,并且您的客户端 javascript 只需对预签名 URL 执行 HTTP PUT。我写了一个 Vue 组件来做这样的事情,S3 upload related code 与库无关,看看就明白了。 在任何 S3 存储桶中用于 HTTP/S POST 上传的另一个第三方。 JS3上传纯html5:jfileupload.com/products/js3upload-html5/index.html 【参考方案1】:

我认为您想要的是使用 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。您的密钥在服务器上保密。

【讨论】:

请注意,这使用了即将被 v4 取代的 Signature v2:docs.aws.amazon.com/AmazonS3/latest/API/… 请务必将$filename 添加到密钥名称中,因此对于上面的示例,user/eric/$filename 而不仅仅是user/eric。如果user/eric 是一个已经存在的文件夹,则上传将静默失败(您甚至会被重定向到success_action_redirect)并且上传的内容将不存在。只是花了几个小时调试这个,认为这是一个权限问题。 @secretmike 如果您在执行此方法时收到超时,您会如何建议绕过该方法? @Trip 由于浏览器正在将文件发送到 S3,因此您需要在 Javascript 中检测超时并自己发起重试。 @secretmike 这闻起来像一个无限循环。因为对于任何超过 10/mbs 的文件,超时将无限期重复。 【参考方案2】:

您可以通过 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 是的。 @Joomler 嗨谢谢,但我在 firefox RequestTimeout 上遇到了这个问题 您与服务器的套接字连接在超时期限内没有被读取或写入。空闲连接将被关闭,文件不会在 S3 上上传。你能帮我解决这个问题吗。谢谢 @usama 你能在 github 上打开这个问题吗,因为我不清楚这个问题 这应该是正确的答案@Olegas【参考方案3】:

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

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

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

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

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

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

这些检查可以“无服务器”完成,因为您可以在桌面上运行它们。 (即 S3 完成所有工作,这些过程只是提醒您注意 S3 存储桶被滥用,因此您不会在月底收到巨大 AWS 账单。)

【讨论】:

伙计,我忘了在 Lambda 之前事情有多复杂。【参考方案4】:

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

这里总结一下:

用户选择要上传的文件后,立即执行以下操作: 1.调用web服务器发起服务生成所需参数

    在该服务中,调用 AWS IAM 服务获取临时凭证

    获得信任后,创建存储桶策略(base 64 编码字符串)。然后使用临时秘密访问密钥对存储桶策略进行签名以生成最终签名

    将必要的参数发送回 UI

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

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

【讨论】:

我花了一整天的时间试图用 Javascript 解决这个问题,这个答案告诉我如何使用 XMLhttprequest 来做到这一点。我很惊讶你被否决了。 OP 要求提供 javascript 并在推荐的答案中获得表格。好伤心。感谢您的回答! BTW superagent 存在严重的 CORS 问题,因此 xmlhttprequest 似乎是目前唯一合理的方法【参考方案5】:

要创建签名,我必须使用我的密钥。但所有的事情 发生在客户端,因此密钥很容易被泄露 来自页面源(即使我混淆/加密了我的源)。

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

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

【讨论】:

但机器人可能会尝试快速上传无限的文件。我可以设置每个存储桶的最大文件数的政策吗?【参考方案6】:

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

步骤:

    要知道如何创建 Create IdentityPoolId http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

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

      创建包含以下代码的 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>
      

【讨论】:

没有人能够使用我的“IdentityPoolId”将文件上传到我的 S3 存储桶。此解决方案如何防止任何第 3 方仅复制我的“IdentityPoolId”并将大量文件上传到我的 S3 存储桶? ***.com/users/4535741/sahil 您可以通过为 S3 存储桶设置适当的 CORS 设置来防止从其他域上传数据/文件。因此,即使有人访问了您的身份池 ID,他们也无法操纵您的 s3 存储桶文件。【参考方案7】:

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

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

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

【讨论】:

【参考方案8】:

以下是使用 node 和 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;

使用的配置对象存储在SSMParameter Store中,如下所示


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

【讨论】:

【参考方案9】:

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

见: https://github.com/auth0-samples/auth0-s3-sample/ 和 auth0 文档。

【讨论】:

据我了解 - 现在我们有 Cognito 了?【参考方案10】:

我创建了一个基于 VueJS 的 UI 和 Go 将二进制文件上传到 AWS Secrets Manager https://github.com/ledongthuc/awssecretsmanagerui

上传受保护的文件和更轻松地更新文本数据很有帮助。需要的可以参考。

【讨论】:

以上是关于从客户端浏览器直接上传 Amazon S3 文件 - 私钥泄露的主要内容,如果未能解决你的问题,请参考以下文章

基于浏览器的上传到 Amazon S3?

如何使用 node.js、Express 和 knox 将文件从浏览器上传到 Amazon S3? [关闭]

ios amazon s3 上传视频文件

Amazon S3 直接上传无法识别文件的内容类型

通过 iframe 上传 Amazon S3

将文件上传到 Amazon s3 - 客户端 javascript