RSSAmplifier

Ayaan's Blog · Nov 28, 2022

Making Cloudfront Signed URLs work with response-content-disposition

0
Sign in to vote or save

Ayaan Zaidi · obviy.us

As of v3.219.0 of the @aws-sdk/cloudfront-signer package, adding a response-content-disposition query parameter to the URL being sent to the getSignedUrl function will result in an Access Denied error on the output Cloudfront URL:

const disposition = 'attachment;filename="test.txt"';
const signedUrl = getSignedUrl({
	keyPairId: config.cloudfrontKeyPairId,
	privateKey: config.cloudfrontSigningKey,
	url: `https://d1x2y3z4.cloudfront.net/test.txt?response-content-disposition=${encodeURIComponent(
		contentDisposition,
	)}"`,
	dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});
console.log(signedUrl);
// https://d1x2y3z4.cloudfront.net/test.txt?response-content-disposition=attachment;filename=newFilename.txt&Policy=XXXXXX&Key-Pair-Id=YYYYYY&Signature=ZZZZZZ

The above code will result in an Access Denied error when the URL is accessed:

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

Evidently, the issue happens when the encoded URL (using encodeURIComponent) is reverted by the getSignedUrl function.

From the source, it seems that the input url parameter is fed into the @aws-sdk/url-parser package, which is presumably where the encoded URL is being lost in translation.

import { parseUrl } from "@aws-sdk/url-parser";
...
/**
 * Creates a signed URL string using a canned or custom policy.
 * @returns the input URL with signature attached as query parameters.
 */
export function getSignedUrl({
  dateLessThan,
  dateGreaterThan,
  url,
  keyPairId,
  privateKey,
  ipAddress,
  policy,
}: CloudfrontSignInput): string {
  const parsedUrl = parseUrl(url);
	...
}

The Workaround

The workaround is to manually set the query parameter after parsing the signed URL:

const disposition = `attachment;filename=newFilename.txt;`;
const url = new URL(`test.txt`, `https://d1x2y3z4.cloudfront.net`);
url.searchParams.append("response-content-disposition", disposition);
const signedUrl = getSignedUrl({
	url: url.href,
	keyPairId: config.cloudfrontKeyPairId,
	privateKey: config.cloudfrontSigningKey,
	dateLessThan: expires.toISOString(),
	dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});
const fixedSignedUrl = new URL(signedUrl);
fixedSignedUrl.searchParams.set("response-content-disposition", disposition);
return fixedSignedUrl.href;

Which works out just fine! 🎉

From the comments on this GitHub issue, a “fix” doesn’t seem to be in the works. I ended up losing many a few hours trying to figure out what was going on. Hoping that this helps out someone else.

Read the original on obviy.us

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.