copyObject Access Denied on S3 Bucket despite policy allowing my site referrer - php

I am using AWS SDK for PHP to upload and display files to/from my S3 bucket.
The files should only be accessible through my site, no other referrer allowed - no hotlinking etc.
I also need to be able to copy objects within the bucket.
I create and connect as normal:
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'eu-west-2'
]);
$s3 = $s3Client::factory(array(
'version' => 'latest',
'region' => 'eu-west-2',
'credentials' => array(
'provider' => $provider,
'key' => $key,
'secret' => $secret
)
));
And execute Copy object command:
$s3->copyObject([
'Bucket' => "{$targetBucket}",
'Key' => "{$targetKeyname}",
'CopySource' => "{$sourceBucket}/{$sourceKeyname}",
]);
I have tried a policy using "Allow if string like referrer" but AWS then tells me I'm allowing public access?!?!?
Everything works just fine EVEN the copyObject action but files are still accessible directly and from everywhere!
I try using "Deny if string not like referrer" which works mostly as expected - I can upload and display the files and the files don't show when linked directly (which is what i want) - However, the copyObject action no longer works and i get the access denied error.
I've tried everything else i can think of and spent hours googling and searching for answers but to no avail.
Here's each seperate policy...
ALLOW FILE ACCESS ONLY (GetObject) if string LIKE referrer:
{
"Version": "2008-10-17",
"Id": "",
"Statement": [
{
"Sid": "Deny access if referer is not my site",
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::MY-BUCKET/*"
],
"Condition": {
"StringLike": {
"aws:Referer": [
"http://MY-SITE/*",
"https://MY-SITE/*"
]
}
}
}
]
}
RESULT: uploads & copyObject work but files are still accessible everywhere
DENY ALL ACTIONS (*) if string NOT LIKE referrer:
{
"Version": "2008-10-17",
"Id": "",
"Statement": [
{
"Sid": "Deny access if referer is not my site",
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::MY-BUCKET/*"
],
"Condition": {
"StringNotLike": {
"aws:Referer": [
"http://MY-SITE/*",
"https://MY-SITE/*"
]
}
}
}
]
}
RESULT: copyObject action no longer works and i get the access denied error

AWS is probably warning you that it's public because it is, for all practical purposes, still public.
Warning
This key should be used carefully: aws:referer allows Amazon S3 bucket owners to help prevent their content from being served up by unauthorized third-party sites to standard web browsers. [...] Since aws:referer value is provided by the caller in an HTTP header, unauthorized parties can use modified or custom browsers to provide any aws:referer value that they choose. As a result, aws:referer should not be used to prevent unauthorized parties from making direct AWS requests. It is offered only to allow customers to protect their digital content, stored in Amazon S3, from being referenced on unauthorized third-party sites.
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
If you are okay with this primitive mechanism, use it, but be aware that all it does is trust what the claim made by the browser.
And that's your problem with copyObject() -- the request is not being made by a browser so there is no Referer header to validate.
You can use the StringLikeIfExists condition test to Deny only wrong referers (ignoring the absence of a referer, as would occur with an object copy) or -- better -- just grant s3:GetObject with StringLike your referer, understanding that the public warning is correct -- this allows unauthenticated access, which is what referer checking amounts to. Your content is still publicly accessible but not from a standard, unmodified web browser if hotlinked from another site.
For better security, you will want to render your HTML with pre-signed URLs (with short expiration times) for your S3 assets, or otherwise do full and proper authorization using Amazon Cognito.

Objects in Amazon S3 are private by default. There is no access unless it is granted somehow (eg on an IAM User, IAM Group or an S3 bucket policy).
The above policies are all Deny policies, which can override an Allow policy. Therefore, they aren't the reason why something is accessible.
You should start by discovering what is granting access, and then remove that access. Once the objects are private again, you should create a Bucket Policy with Allow statements that define in what situations access is permitted.

Related

Google Cloud Translate API & Referer Restriction Issue

I have a frustrating issue with the Google Cloud Translate API.
I set up correctly the restriction of the key to some domains including *.example.com/ * (without blank space at the end)
I launch the script on the URL https://www.example.com/translate and i have the following message :
"status": "PERMISSION_DENIED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "API_KEY_HTTP_REFERRER_BLOCKED",
"domain": "googleapis.com",
When i remove the restriction, everything works, but i need the restriction to avoid misuse/abuse.
Furthemore, i use this same API Key for others Google App API (Maps, Auth, etc) and it works perfectly from this domain...
So weird.
Do you have any ideas or any ways to investigate better this issue ?
How i can know the referrer Google sees ? (or any external service)
Thanks a lot !!
Edit :
PHP code :
require_once(APPPATH . "libraries/GoogleTranslate/vendor/autoload.php");
require_once(APPPATH . "libraries/GoogleTranslate/vendor/google/cloud-translate/src/V2/TranslateClient.php");
$translate = new TranslateClient([
'key' => 'xXXXx'
]);
// Translate text from english to french.
$result = $translate->translate('Hello world!', [
'target' => 'fr'
]);
echo $result['text'];
Full error message :
Type: Google\Cloud\Core\Exception\ServiceException
Message: {
"error": { "code": 403, "message": "Requests from referer
\u003cempty\u003e are blocked.",
"errors": [ { "message": "Requests from referer \u003cempty\u003e are blocked.", "domain": "global", "reason": "forbidden" } ],
"status": "PERMISSION_DENIED",
"details": [ { "#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "API_KEY_HTTP_REFERRER_BLOCKED",
"domain": "googleapis.com",
"metadata": { "service": "translate.googleapis.com", "consumer": "projects/XXXXX" } } ] } }
Filename: htdocs/application/libraries/GoogleTranslate/vendor/google/cloud-core/src/RequestWrapper.php
Line Number: 368
I will leave here my insights discussed on the Public Issue Tracker.
The HTTP restriction is working as intended, but the referer is always empty because this is not set by default. However, it can be added manually, so instead of doing:
-$translate = new TranslateClient([
'key' => 'XXX'
]);
You need to specify the referrer:
-$translate = new TranslateClient([
'key' => '[API_KEY]',
'restOptions' => [
'headers' => [
'referer' => '*.[URL].com/*'
]
]
]);
You have to take into account that this type of requests can be sent from whatever computer (if you have the key) since you’re not restricting the domain where the request is made, only checking who is the referrer (and you can set it manually). Moreover, API clients that run on a web browser expose their API keys publicly; that’s why I recommend you to use service accounts instead. For more information: adding application restrictions.
Regarding the HTTP referer, this is basically a header field that, basically, the web browsers put to let the web page know where the user is coming from. For example, if you click the above link (HTTP referer) your referer field will be this page.
In summary, since you can put whatever referer in the header of a request, this is pretty similar to not having any type of restrictions. Indeed, it’s recommended to use service accounts. To solve this issue easily, add the referer manually in the headers as exposed in the code above.
I read the comments and you seem to be doing everything ok. I would recommend you to try:
This error message can appear because you set API restrictions in the API key, is this the case? Maybe you’re restricting this specific API.
If you aren’t setting any API restrictions, is it possible to try adding an IP instead of the domain just for testing purposes?
I had same issue with google translate but not with maps.
So maps works with referrer restriction, but translate does not.
The only solution I found, with a restriction in force, is setting up an IP restriction instead of the HTTP referrers (web sites).

Content field is Empty when Accessing through AWS PHP SDK and AWS AppConfig

Everyone does anyone have an idea of how to use AWS AppConfig with the AWS SDK PHP.
My Particular use case is, I am running a simple PHP app on the EC2 instance and want to receive the JSON configurations written in the AppConfig.
use Aws\AppConfig\Exception\AppConfigException;
use Aws\AppConfig\AppConfigClient;
$appConfigClient = new AppConfigClient(['version' => 'latest', 'region' => 'ap-south-1']);
$clientid = uniqid('', true);
$params = [
'Application' => $APP_CONFIG_APP,
'ClientId' => $clientid,
'Configuration' => $APP_CONFIG_CONFIGURATION_PROFILE,
'Environment' => $APP_CONFIG_ENVIRONMENT
];
$response = $appConfigClient->getConfiguration($params);
$config = $response['Content'];
Also, I am authorizing with the AppConfig by an Administrator IAM Role Provided so, no issues on that side and I am able to get the following output
{ "Content": {}, "ConfigurationVersion": "1", "ContentType": "application\/octet-stream", "#metadata": { "statusCode": 200, client_id=60696", and some more fields...}
But the issue is I am not getting the AppConfig Content but able to extract the metadata regarding the Data.
So, Anyone who had tried this please help me out here.
I haven't used AppConfig yet but am reading up about it at the minute.
If you look at the information in the 'Important' box on the page linked below then it says that the content section is not returned if there is no change to the appconfig data.
So maybe the original request had data but then any subsequent request is not detecting a change and therefore not sending any content through
It is probably worth updating the data in AppConfig and then running a new request to see if the content is populated as required.
See:
https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-retrieving-the-configuration.html

PHP AWS SDK | How to create signed URL with custom domain

I'm using AWS SDK with Laravel framework in PHP. Here is my code
$cloudFront = new CloudFrontClient([
'region' => env('AWS_REGION'),
'version' => 'latest'
]);
$path = "R180417XXXX.mp4"
$resourceURL = "https://dbk93n3xxxxxx.cloudfront.net/" . $path;
$expires = Carbon::now()->addMinutes(5)->timestamp;
$signedUrlCannedPolicy = $cloudFront->getSignedUrl([
'url' => $resourceURL,
'expires' => $expires,
'private_key' => base_path('pk-APKAI2PXXXXXXXXXXXXX.pem'),
'key_pair_id' => 'APKAI2PXXXXXXXXXXXXX',
]);
This code is working but the URL it look like this
https://dbk93n3xxxxxx.cloudfront.net/R180417XXXX.mp4?Expires=1524389577&Signature=RmBDMqM4SMadsQstrgVpUiLoJ50dvKoxNI081Joa7WjSg5eelziQqtDrcs~klbDHvs7rMaq2McfHUQijrcLe7F9tDbn7oOxEC4kfPPCMbhqqjtBWavPmM8Zv8QhH50dPuNHwnEj4pIGUpm9FmAvDhCSExCv0uBMWUREJ9YKQJFHZcPJyKBtjPcJVzIGpnj2bQn3xNGO5AUlutsyeSWUqdvtNOLb3xurgx4WzcVotgB~BZo-bQxo3ieXFbKWAPQXMPl93YpuX5W10l4YtYPULrAtJVQZKUIFcfifnECnqg~IgtbkFbyLdM5e87ZiC837Hj-AphmlEshnY-MHWyEU24g__&Key-Pair-Id=APKAI2PXXXXXXXXXXXXX
But I'm just setting CNAME in CloudFront like server1.domain.tld I want the signed URL show like
https://server1.domain.tld/R180417XXXX.mp4?Expires=1524389577&Signature=RmBDMqM4SMadsQstrgVpUiLoJ50dvKoxNI081Joa7WjSg5eelziQqtDrcs~klbDHvs7rMaq2McfHUQijrcLe7F9tDbn7oOxEC4kfPPCMbhqqjtBWavPmM8Zv8QhH50dPuNHwnEj4pIGUpm9FmAvDhCSExCv0uBMWUREJ9YKQJFHZcPJyKBtjPcJVzIGpnj2bQn3xNGO5AUlutsyeSWUqdvtNOLb3xurgx4WzcVotgB~BZo-bQxo3ieXFbKWAPQXMPl93YpuX5W10l4YtYPULrAtJVQZKUIFcfifnECnqg~IgtbkFbyLdM5e87ZiC837Hj-AphmlEshnY-MHWyEU24g__&Key-Pair-Id=APKAI2PXXXXXXXXXXXXX
I'm have been tried to change $resourceURL to
$resourceURL = "https://server1.domain.tld/" . $path;
It's not working.
It's response status code 403 and I has been set Origin Access Identity I don't know why not working
Here is my Amazon S3 Policy
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E2OP22ZEXXXXXX"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::server1.domain.tld/*"
}
]
}
Please help...
Thanks
In Route53, there needs to be a hosted zone for your tld and a record set of type CNAME that is an alias to Cloudfront distribution.
Here are steps to follow:
Create certificate in Certificates Manager for domain.tld and server1.domain.tld.
Edit your Cloudfront Distribution Settings and set SSL certificate for the distribution to the custom one.
Ensure that Alternate Domain Names (CNAMEs) for your distribution lists server1.domain.tld
Create Public Hosted Zone for domain.tld in Route53
Copy 4 Nameservers and update your domain registrar to point to them if domain name wasn't setup originally in Route 53
Create Record Set in the Hosted Zone for a CNAME alias that points to Cloudfront Distribution.
Finally, rest easy and see changes propagate to name servers et Viola!

S3 ,verify a signed URL in PHP, Deconstruct signed URL

I am using s3 direct uploads along with a database to store the URLS of the files (along with other data like who uploaded etc).
To allow direct upload to s3, I'm creating a presigned URL like :
$s3 = App::make('aws')->createClient('s3', [
'credentials' => [
'key' => 'AAAAAAAAAAAAAAA',
'secret' => 'YYYYYYYYYYYYYYYYYYYY',
]
]);
$command = $s3->getCommand('PutObject', [
'#use_accelerate_endpoint'=>true,
'Bucket' => 'remdev-experimental',
'Key' => "newest newest.txt",
'Metadata' => array(
'foo' => "test",
)
]);
return response()->json(((string)$s3->createPresignedRequest($command, '+1 minutes')->getUri()));
Now, after the file from the client has finished uploading , I want my server to know about it. So I will require the client to send me a request , notifying about the fact that he has finished uploading. For this, I think the simplest(and also secure) way is to just allow the client to send back the signed URL that he just sent back.
Is there a way to parse the URL ?
I am interested in getting the object key , and more importantly , I want to verify that the URL has not been tampered with (meaning, the signature in the URL should match the rest of the contents). How can I do this in php sdk ?
The signed URL is the file's URL with the signature information in the query data. So a signed request for bucket: remdev-experimental file: abc.txt looks like https://s3.amazonaws.com/remdev-experimental/abc.txt?X-Amz-Date=date&X-Amz-Expires=60&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=signature&X-Amz-Credential=SOMEID/20160703/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=Host&x-amz-security-token=some-long-token so all you need to do is get the URL's Path (/remdev-experimental/abc.txt and take everything after the 2nd slash.
Also you should be aware that you can have S3 redirect the browser to a URL using success_action_redirect in an HTTP post policy
Lastly you can have S3 trigger a notification to your server (via SQS, SNS, or Lambda) whenever a file is uploaded.

Browser uploads to s3 with instance roles

This is a follow-up on my previous question regarding policy document signing using instance profiles.
I'm developing a system that allows drag & drop uploads directly to an S3 bucket; an AJAX request is first made to my server containing the file metadata. Once verified, my server responds with the form parameters that are used to complete the upload.
The process of setting up browser based uploads is well explained here and it all works as expected in my local test environment.
However, once my application gets deployed on an EC2 instance, I'm seeing this error when the browser attempts to upload the file:
<Error>
<Code>InvalidAccessKeyId</Code>
<Message>The AWS Access Key Id you provided does not exist in our records.</Message>
<RequestId>...</RequestId>
<HostId>...</HostId>
<AWSAccessKeyId>ASIAxxxyyyzzz</AWSAccessKeyId>
</Error>
The value of ASIAxxxyyyzzz here comes from the instance role credentials, as obtained from the metadata service; it seems that those credentials can't be used outside of EC2 to facilitate browser based uploads.
I've looked at the Security Token Service as well to generate another set of temporary credentials by doing this:
$token = $sts->assumeRole(array(
'RoleArn' => 'arn:aws:iam::xyz:role/mydomain.com',
'RoleSessionName' => 'uploader',
));
$credentials = new Credentials($token['Credentials']['AccessKeyId'], $token['Credentials']['SecretAccessKey']);
The call givens me a new set of credentials, but it give the same error as above when I use it.
I hope that someone has done this before and can tell me what stupid thing I've missed out :)
The AWS docs are very confusing on this, but I suspect that you need to include the x-amz-security-token parameter in the S3 upload POST request and that its value matches the SessionToken you get from STS ($token['Credentials']['SessionToken']).
STS temporary credentials are only valid when you include the corresponding security token.
The AWS documentation for the POST request states that:
Each request that uses Amazon DevPay requires two x-amz-security-token
form fields: one for the product token and one for the user token.
but that parameter is also used outside of DevPay, to pass the STS token and you would only need to pass it once in the form fields.
As pointed out by dcro's answer, the session token needs to be passed to the service you're using when you use temporary credentials. The official documentation mentions the x-amz-security-token field, but seems to suggest it's only used for DevPay; this is probably because DevPay uses the same type of temporary credentials and therefore requires the session security token.
2013-10-16: Amazon has updated their documentation to make this more obvious.
As it turned out, it's not even required to use STS at all; the credentials received by the metadata service come with such a session token as well. This token is automatically passed for you when the SDK is used together with temporary credentials, but in this case the final request is made by the browser and thus needs to be passed explicitly.
The below is my working code:
$credentials = Credentials::factory();
$signer = new S3Signature();
$policy = new AwsUploadPolicy(new DateTime('+1 hour', new DateTimeZone('UTC')));
$policy->setBucket('upload.mydomain.com');
$policy->setACL($policy::ACL_PUBLIC_READ);
$policy->setKey('uploads/test.jpg');
$policy->setContentType('image/jpeg');
$policy->setContentLength(5034);
$fields = array(
'AWSAccessKeyId' => $credentials->getAccessKeyId(),
'key' => $path,
'Content-Type' => $type,
'acl' => $policy::ACL_PUBLIC_READ,
'policy' => $policy,
);
if ($credentials->getSecurityToken()) {
// pass security token
$fields['x-amz-security-token'] = $credentials->getSecurityToken();
$policy->setSecurityToken($credentials->getSecurityToken());
}
$fields['signature'] = $signer->signString($policy, $credentials);
I'm using a helper class to build the policy, called AwsUploadPolicy; at the time of writing it's not complete, but it may help others with a similar problem.
Permissions were the last problem; my code sets the ACL to public-read and doing so requires the additional s3:PutObjectAcl permission.
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Sid": "Stmt1379546195000",
"Resource": [
"arn:aws:s3:::upload.mydomain.com/uploads/*"
],
"Effect": "Allow"
}
]
}

Categories