Kousha
Kousha

Reputation: 36269

AWS S3 Presigned URL with other query parameters

I create a pre-signed URL and get back something like

https://s3.amazonaws.com/MyBucket/MyItem/
?X-Amz-Security-Token=TOKEN
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20171206T014837Z
&X-Amz-SignedHeaders=host
&X-Amz-Expires=3600
&X-Amz-Credential=CREDENTIAL
&X-Amz-Signature=SIGNATURE

I can now curl this no problem. However, if I now add another query parameter, I will get back a 403, i.e.

https://s3.amazonaws.com/MyBucket/MyItem/
?X-Amz-Security-Token=TOKEN
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20171206T014837Z
&X-Amz-SignedHeaders=host
&X-Amz-Expires=3600
&X-Amz-Credential=CREDENTIAL
&X-Amz-Signature=SIGNATURE
&Foo=123

How come? Is it possible to generate a pre-signed url that supports custom queries?

Upvotes: 7

Views: 10832

Answers (5)

wintan
wintan

Reputation: 66

If you are looking on for JavaScript SDK V3:

import { HttpRequest } from "@aws-sdk/protocol-http";
import { S3RequestPresigner } from "@aws-sdk/s3-request-presigner";
import { parseUrl } from "@aws-sdk/url-parser";
import { Sha256 } from "@aws-crypto/sha256-browser";
import { Hash } from "@aws-sdk/hash-node";
import { formatUrl } from "@aws-sdk/util-format-url";

// Make custom query in Record<string, string | Array<string> | null> format
const customQuery = {
  hello: "world",
};

const s3ObjectUrl = parseUrl(
  `https://${bucketName}.s3.${region}.amazonaws.com/${key}`
);
s3ObjectUrl.query = customQuery; //Insert custom query here

const presigner = new S3RequestPresigner({
  credentials,
  region,
  sha256: Hash.bind(null, "sha256"), // In Node.js
  //sha256: Sha256 // In browsers
});

// Create a GET request from S3 url.
const url = await presigner.presign(new HttpRequest(s3ObjectUrl));
console.log("PRESIGNED URL: ", formatUrl(url));

Code template taken from: https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/

Upvotes: 1

eprothro
eprothro

Reputation: 1117

While not documented, you can add parameters as arguments to the call to presigned_url.

obj.presigned_url(:get, 
  expires_in: expires_in_sec, 
  response_content_disposition: "attachment"
)
https://bucket.s3.us-east-2.amazonaws.com/file.txt?response-content-disposition=attachment&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=PUBLICKEY%2F20220309%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20220309T031958Z&X-Amz-Expires=43200&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATUREVALUE

Upvotes: 0

David Rissato Cruz
David Rissato Cruz

Reputation: 3657

I created this solution for Ruby SDK. It is sort of a hack, but it works as expected:

require 'aws-sdk-s3'
require 'active_support/core_ext/object/to_query.rb'

# Modified S3 pre signer class that can inject query params to the URL
#
# Usage example:
#
#    bucket_name = "bucket_name"
#    key = "path/to/file.json"
#    filename = "download_file_name.json"
#    duration = 3600
#
#    params = {
#      bucket: bucket_name,
#      key: key,
#      response_content_disposition: "attachment; filename=#{filename}",
#      expires_in: duration
#    }
#
#    signer = S3PreSignerWithQueryParams.new({'x-your-custom-field': "banana", 'x-some-other-field': 1234})
#    url = signer.presigned_url(:get_object, params)
#
#    puts "url = #{url}"
#
class S3PreSignerWithQueryParams < Aws::S3::Presigner
  def initialize(query_params = {}, options = {})
    @query_params = query_params
    super(options)
  end

  def build_signer(cfg)
    signer = super(cfg)
    my_params = @query_params.to_h.to_query()
    signer.define_singleton_method(:presign_url,
                                   lambda do |options|
                                     options[:url].query += "&" + my_params
                                     super(options)
                                   end)
    signer
  end
end

Upvotes: 0

jarmod
jarmod

Reputation: 78842

It seems to be technically feasible to insert custom query parameters into a v4 pre-signed URL, before it is signed, but not all of the AWS SDKs expose a way to do this.

Here's an example of a roundabout way to do this with the AWS JavaScript SDK:

const AWS = require('aws-sdk');

var s3 = new AWS.S3({region: 'us-east-1', signatureVersion: 'v4'});
var req = s3.getObject({Bucket: 'mybucket', Key: 'mykey'});
req.on('build', () => { req.httpRequest.path += '?session=ABC123'; });
console.log(req.presign());

I've tried this with custom query parameters that begin with X- and without it. Both appeared to work fine. I've tried with multiple query parameters (?a=1&b=2) and that worked too.

The customized pre-signed URLs work correctly (I can use them to get S3 objects) and the query parameters make it into CloudWatch Logs so can be used for correlation purposes.

Note that if you want to supply a custom expiration time, then do it as follows:

const Expires = 120;
const url = req.presign(Expires);

I'm not aware of other (non-JavaScript) SDKs that allow you to insert query parameters into the URL construction process like this so it may be a challenge to do this in other languages. I'd recommend using a small JavaScript Lambda function (or API Gateway plus Lambda function) that would simply create and return the customized pre-signed URL.

The custom query parameters are also tamper-proof. They are included in the signing of the URL so, if you tamper with them, the URL becomes invalid, yielding 403 Forbidden.

I used this code to generate your pre-signed URL. The result was:

https://s3.amazonaws.com/MyBucket/MyItem
?Foo=123
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA...27%2Fus-east-1%2Fs3%2Faws4_request
&X-Amz-Date=20180427T0012345Z
&X-Amz-Expires=3600
&X-Amz-Signature=e3...7b
&X-Amz-SignedHeaders=host

None of this is a guarantee that this technique will continue to work, of course, if AWS changes things under the covers but for right now it seems to work and is certainly useful.

Attribution: the source of this discovery was aws-sdk-js/issues/502.

Upvotes: 8

John Hanley
John Hanley

Reputation: 81454

If you change one of the headers or add / subtract, then you have to resign the URL.

This is part of the AWS signing design and this process is designed for higher levels of security. One of the AWS reasons for changing to signing version 4 from signing version 2.

The signing design does not know which headers are important and which are not. That would create a nightmare trying to track all of the AWS services.

Upvotes: 5

Related Questions