aws-sdk-js-v3 icon indicating copy to clipboard operation
aws-sdk-js-v3 copied to clipboard

CloudFront Signed URL returns 403 with content-disposition query parameter

Open CodyDunlap opened this issue 2 years ago • 7 comments

Describe the bug

Providing the response-content-disposition query parameter in a CloudFront Signed URL using the new cloudfront-signer package results in a 403 if the query parameter contains a filename option. For example, using the query parameter value 'attachment; filename=test.mp4;' results in a 403. However, if I simply use the value attachment; the download works fine, except that it doesn't change the download filename as intended.

Your environment

I have an API Gateway that is using a Lambda function to create the cloudfront url and sign it before returning to the browser. The url returned to the browser has been signed and contains the urlEncoded query parameter for response-content-disposition.

SDK version number

@aws-sdk/[email protected]

Is the issue in the browser/Node.js/ReactNative?

I'm not sure if this is a problem with the SDK in node or if it's a bug with Cloudfront. I've seen some reports with other libraries that may have the same issue, potentially indicating an issue with Cloudfront. In those reports it seems to be that specifically the query parameter doesn't work as expected. However, using a header may resolve the issue. But the cloudfront-signer library does not allow me to provide headers like this.

Details of the browser/Node.js/ReactNative version

Node v14.18.0

Steps to reproduce

Please share code or minimal repo, and steps to reproduce the behavior.

Using a CloudFront distribution with an S3 origin, the following JS is used to create the disposition query parameter and sign the url.

const disposition = `attachment; filename=test.mp4;`;

const url = new URL(s3Object.path, `https://${cloudfrontDomain}`);
url.searchParams.append("response-content-disposition", encodeURIComponent(disposition));

info.url = getSignedUrl({
  url: url.toString(),
  keyPairId: cloudFrontPublicKeyId,
  privateKey: cloudFrontPrivateKey,
  dateLessThan: new Date().setUTCMinutes(new Date().getUTCMinutes() + 5).toISOString()
});

Observed behavior

A clear and concise description of what happens.

I receive the following response:

<Error>
  <Code>AccessDenied</Code>
  <Message>Access denied</Message>
</Error>

Expected behavior

A clear and concise description of what you were expecting to happen.

I expect the request to begin a download of a file named test.mp4

Screenshots

If applicable, add screenshots to help explain your problem.

Additional context

Add any other context about the problem here.

CodyDunlap avatar May 17 '22 16:05 CodyDunlap

We discovered a workaround for this.

The issue is that the special characters such as " and = in the query parameter need to be encoded in the request, but the getSignedUrl function is reverting those encodings. This means that the url returned by the function does not match the url that was signed, and thus results in a 403.

For example, passing the url https://<myclouddfrontdomain>/<filepath>?response-content-disposition=filename%3Dtest.mp4 is signed as such, but the url that is returned from the function is https://<myclouddfrontdomain>/<filepath>?response-content-disposition=filename=test.mp4 which obviously doesn't match.

To work around this we implemented the following solution:

const disposition = `attachment; filename=Test File.mp4;`;

const url = new URL(s3Key, `https://${cfDomain}`);
url.searchParams.append("response-content-disposition", disposition);

const signedUrl = getSignedUrl({
  url: url.href,
  keyPairId: cfPubKeyId,
  privateKey: privateKey,
  dateLessThan: expires.toISOString()
});

const fixedSignedUrl = new URL(signedUrl);
fixedSignedUrl.searchParams.set("response-content-disposition", disposition);
return fixedSignedUrl.href;

CodyDunlap avatar May 17 '22 18:05 CodyDunlap

Hey @CodyDunlap thanks for providing the workaround I was able to use the same, I'll need to dive deeper to check if the fix can be made for this as it seems to be more like a usage issue than an actual bug.

ajredniwja avatar Oct 03 '22 06:10 ajredniwja

Hey @ajredniwja, I was recently working with this library again and was curious if there was any update on this issue? Will it be resolved in a future update of the signer package?

CodyDunlap avatar Nov 21 '22 18:11 CodyDunlap

@CodyDunlap many thanks for your workaround. Saved me a headache. @ajredniwja if a fix is not possible, an update to the docs would be a huge help.

obviyus avatar Nov 28 '22 11:11 obviyus

@CodyDunlap Thank you so much for this solution! I owe ya :D

johnkoehn avatar Oct 04 '23 01:10 johnkoehn

@ajredniwja seems like this has taken a backseat. Considering how important it is to write robust code around the use of content-disposition, I am very worried that if I use the above workaround, it will break under my feet at a later time.

Can we please have some more urgent attention on this issue?

jpike88 avatar Nov 14 '23 09:11 jpike88

I'm having a similar issue when trying to set a ContentDistribution when creating a signed url for storing an object in an s3 bucket.

Can't seems to find a work around, getting a 403 as soon as I'm adding it!

Rieranthony avatar Jan 05 '24 13:01 Rieranthony