亚马逊 S3直接文件上传从客户端浏览器-私钥公开

我正在实现一个直接的文件上传从客户机机器到亚马逊 S3通过 REST API 只使用 JavaScript,没有任何服务器端代码。一切都很好,但有一件事让我担心。

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

我该怎么办?这有什么问题吗?也许我可以将特定私钥的使用限制为只能从特定 CORS Origin 调用 REST API,并且只能使用 PUT 和 POST 方法,或者也许只能将 key 链接到 S3和特定 bucket?可能还有其他的认证方法?

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

148559 次浏览

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

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

您最好的选择是为您的 javascript 客户机创建一个带有密钥的“ IAM 用户”。只允许它对一个桶进行写访问。(但是理想情况下,不要启用 ListBucket 操作,这会使它对攻击者更具吸引力。)

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

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

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

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

我认为您需要的是基于浏览器的使用 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 操作正在发送文件 直达中三-而不是通过您的服务器。

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

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

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

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

因此,我建议,只需创建一个特殊的 S3 bucket,它是公共可写的(但不可读) ,这样就不需要客户端上的任何签名组件。

桶名(GUID)将是您对抗恶意上传的唯一防御(但是潜在的攻击者不能使用您的桶来传输数据,因为它只写给他)

要创建一个签名,我必须使用我的秘钥。但所有的东西 发生在客户端,因此,密钥可以很容易地显示 从页面源(即使我混淆/加密我的源)。

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

像这样的数字签名用于网络上的安全。如果有人(国家安全局?)真的能够打破他们,他们会有更大的目标比你的 S3桶:)

您可以通过 AWS S3 Cognoto 来实现这一点 试试这个链接:

Http://docs.aws.amazon.com/awsjavascriptsdk/guide/browser-examples.html#amazon_s3

也试试这个代码

只需更改“区域”、“标识池”和“桶名”即可

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

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

参见: Https://github.com/auth0-samples/auth0-s3-sample/
以及 auth0文档。

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

总结如下:

用户选择要上载的文件后,立即执行下列操作: 1. 调用 Web 服务器来启动一个服务来生成所需的参数

  1. 在此服务中,调用 AWS IAM 服务以获得临时信任

  2. 一旦获得信任,就创建一个 bucket 策略(基于64编码的字符串)。然后使用临时秘密访问密钥对 bucket 策略签名,以生成最终签名

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

  4. 一旦接收到这个对象,创建一个 html 表单对象,设置所需的参数并 POST 它。

详情请参阅 Https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/

我给出了一个简单的代码,可以将文件从 Javascript 浏览器上传到 AWS S3,并列出 S3 bucket 中的所有文件。

步骤:

  1. 要了解如何创建 Create IdentityPoolId < a href = “ http://docs.aws.amazon.com/cognto/best/developerguide/Identity-pools.html”rel = “ nofollow noReferrer”> http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Goto S3的控制台页面,从 bucket 属性打开 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. 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>
      

下面介绍如何使用 node 和 无服务器生成策略文档

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

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

上传一个安全的文件和更新文本数据非常有帮助。如果你愿意,你可以参考。