15

In Brief

In order to keep the uploaded media (S3 objects) private for all the clients on my multi-tenant system I implemented a Cloudfront CDN deployment and configured it (and its Origin S3 Bucket) to force the use of signed URLs in order to GET any of the objects.


The Method

First, the user is authenticated via my system, and then a signed URL is generated and returned to them using the AWS.CloudFront.Signer.getSignedUrl() method provided by the AWS JS SDK. so they can make the call to CF/S3 to download the object (image, PDF, docx, etc). Pretty standard stuff.


The Problem

The above method works 95% of the time. The user obtains a signed URL from my system and then when they make an XHR to GET the object it's retrieved just fine.

But, 5% of the time a 403 is thrown with a CORS error stating that the client origin is not allowed by Access-Control-Allow-Origin.

The error, from Safari in this case.

This bug (error) has been confirmed across all environments: localhost, dev.myapp.com, prod.myapp.com. And across all platforms/browsers.

There's such a lack of rhyme or reason to it that I'm actually starting to think this is an AWS bug (they do happen, from time-to-time).


The Debugging Checklist So Far

I've been going out of my mind for days now trying to figure this out. Here's what I've attempted so far:

Have you tried a different browser/platform?

Yes. The issue is present across all client origins, browsers (and versions), and all platforms.

Is your S3 Bucket configured for CORS correctly?

Yes. It's wide-open in fact. I've even set <MaxAgeSeconds>0</MaxAgeSeconds> in order to prevent cacheing of any pre-flight OPTIONS requests by the client:

CORS settings

Is the signed URL expired?

Nope. All of the signed URLs are set to expire 24hrs after generation. This problem has shown up even seconds after any given signed URL is generated.

Is there an issue with the method used to generate the signed URLs?

Unlikely. I'm simply using the AWS.CloudFront.Signer.getSignedUrl() method of their JS SDK. The signed URLs do work most of the time, so it would seem very strange that it would be an issue with the signing process. Also, the error is clearly a CORS error, not a signature mis-match error.

Is it a timezone/server clock issue?

Nope. The system does serve users across many timezones, but that theory proved to be false given that the signed URLs are all generated on the server-side. The timezone of the client doesn't matter, it gets a signed URL good for 24hrs from the time of generation no matter what TZ it's in.

Is your CF distro configured properly?

Yes, so far as I can make out by following several AWS guides, tutorials, docs and such.

Here's a screenshot for brevity. You can see that I've disabled cacheing entirely in an attempt to rule that out as a cause:

CF distro config

Are you seeing this error for all mime-types?

No. This error hasn't been seen for any images, audio, or video files (objects). With much testing already done, this error only seems to show up when attempting to GET a document or PDF file (.doc, .docx, .pdf). This lead me to believe that this was simply an Accept header mis-match error: The client was sending an XHR with the the header Accept: pdf, but really the signature was generated for Accept: application/pdf. I haven't yet been able to fully rule this out as a cause. But it's highly unlikely given that the errors are intermittent. So if it were a Accept header mis-match problem then it should be an error every time.

Also, the XHR is sending Accept: */* so it's highly unlikely this is where the issue is.



The Question

I've really hit a wall on this one. Can anyone see what I'm missing here? The best I can come up with is that this is some sort of "timing" issue. What sort of timing issue, or if it even is a timing issue, I've yet to figure out.

Thanks in advance for any help.

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
AJB
  • 7,389
  • 14
  • 57
  • 88
  • @sideshowbarker I'm not really following what you're saying. Yes, the error is thrown by the browser (Safari in this case). But that's because the client (browser) is receiving a 403 from the server. You seem to have just kinda repackaged what I said in my question. Are you saying that this isn't a CORS issue? – AJB Feb 10 '19 at 02:58
  • To be clear, I'm not looking to "programatically handle" the 403 error, I'm looking to eliminate the 403 error because it should not exist given the context I described above. – AJB Feb 10 '19 at 02:59
  • Yes, I’m saying it’s not a CORS issue. It’s instead just a 403 issue unrelated to your CORS config. – sideshowbarker Feb 10 '19 at 02:59
  • I'm not seeing how that could be the case. If it were a permissions issue than it should fail every time. – AJB Feb 10 '19 at 03:00
  • @sideshowbarker Can you clarify how you came to the conclusion that the 403 is not CORS-related? Have you run into this intermittent issue before? Is there something about the config I illustrated above that leads you to believe this is a simple permissions error that doesn't actually have anything to do with CORS? – AJB Feb 10 '19 at 03:44
  • The CORS protocol doesn’t define any behavior requiring a server to respond with a 403. Ever. So while I suppose it could be Cloudfront has some behavior that causes it to respond with a 403 based on how you’ve configured it to set the Access-Control-Allow-Origin header, that wouldn’t be something defined by the CORS protocol but instead some non-standard behavior in Cloudfront. Anyway, I’d think you’d not need to speculate but could instead check your Cloudfront server logs to locate times when it’s responding with a 403, & see what messages are logged there to indicate what triggered the 403 – sideshowbarker Feb 10 '19 at 04:36
  • 2
    If the origin *"is not allowed by `Access-Control-Allow-Origin`"* then the pressing question seems to be **what does `Access-Control-Allow-Origin` actually contain?** And, what other response headers are included? Of particular interest in addition to any CORS headers would be `Age`, `X-Cache`, and `Server`. – Michael - sqlbot Feb 10 '19 at 07:10
  • 1
    *"The client was sending an XHR with the the header `Content-Type: pdf`"* ...is a confusing statement, since the client should not be sending **any** `Content-Type` header with a `GET` request. The `Content-Type` request header specifies the MIME type of the request body, and `GET` requests have no request body. – Michael - sqlbot Feb 10 '19 at 07:17
  • @Michael-sqlbot I've edited my question and fixed the `Content-Type` mis-statement in the "mime-types paragraph". I meant to write `Accept` header. Got this conflated in my head with previous debugging of S3 uploads. – AJB Feb 10 '19 at 09:15
  • @Michael-sqlbot Regarding the `Access-Control-Allow-Origin` response header contents ... I' m working on it. This bug is so intermittent I haven't been able to reliably recreate it over days. Currently digging through logs, again. – AJB Feb 10 '19 at 09:23
  • 1
    @AJB Have you made any progress on this? I'm running into the exact same issue, except I'm not even using CloudFront (server returns a signed S3 url, and browser does a PUT to that URL). It only happens about 5-10% of the time, and my CORS is wide open as well. – sflogen Mar 15 '19 at 00:57
  • @sflogen Not yet unfortunately. My situation is complicated because I'm also seeing 206s for some reason. I implemented a kludge in my UI for now to work-around the issue. Very frustrating. – AJB Mar 15 '19 at 01:39
  • 1
    We are facing the same issue. Not an actual solution, but invalidating the Cloudfront cache fixes the issue. We are planning to add a monitoring script which frequently checks this and invalidate the cache in case of failures. – Abhishek Garg Jul 19 '19 at 20:04
  • @AbhishekGarg I ran into this issue again a few days ago and tried the CF cache invalidation, but it didn't work for me. I see your answer below, gonna work on that and hopefully it works. Nice to know that I'm not the only one going out of my mind ;) – AJB Oct 11 '19 at 10:33
  • Did you solve it? I'm having the same issue and I've not been able to replicate :( – Andres Espinosa Mar 31 '21 at 15:04
  • 1
    @AndresEspinosa Unfortunately no, I haven't. I did try the solution outlined in Abishak Garg's answer below, but it didn't work for me. It would be worth it to investigate the ServerFault question linked as well though. (And please post-up if you find a solid solution!) – AJB Apr 01 '21 at 20:11

1 Answers1

12

Found the solution for the same on serverfault.

https://serverfault.com/questions/856904/chrome-s3-cloudfront-no-access-control-allow-origin-header-on-initial-xhr-req

You apparently cannot successfully fetch an object from HTML and then successfully fetch it again with as a CORS request with Chrome and S3 (with or without CloudFront), due to peculiarities in the implementations.

Adding the answer from original post so that it does not get lost.

Workaround:

This behavior can be worked-around with CloudFront and Lambda@Edge, using the following code as an Origin Response trigger.

This adds Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origin to any response from S3 that has no Vary header. Otherwise, the Vary header in the response is not modified.

'use strict';

// If the response lacks a Vary: header, fix it in a CloudFront Origin Response trigger.

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    if (!headers['vary'])
    {
        headers['vary'] = [
            { key: 'Vary', value: 'Access-Control-Request-Headers' },
            { key: 'Vary', value: 'Access-Control-Request-Method' },
            { key: 'Vary', value: 'Origin' },
        ];
    }
    callback(null, response);
};
Abhishek Garg
  • 2,158
  • 1
  • 16
  • 30