繁体   English   中英

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

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

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

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

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

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

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

您可以通过 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

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

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

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

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

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

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

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

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

将在这里总结:

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

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

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

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

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

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

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

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

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

我给出了一个简单的代码来将文件从 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>

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

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

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

以下是使用节点和无服务器生成策略文档的方法

"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",
}

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

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

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

上传受保护的文件并更轻松地更新文本数据会很有帮助。 如果你愿意,你可以参考。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM