http_aws_sigv4: canonicalize the query

Percent encoding needs to be done using uppercase, and most
non-alphanumerical must be percent-encoded.

Fixes #11794
Reported-by: John Walker
Closes #11806
This commit is contained in:
Daniel Stenberg 2023-09-06 10:14:44 +02:00
parent e92edfbef6
commit fc76a24c53
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2

View File

@ -44,16 +44,16 @@
#include "slist.h" #include "slist.h"
#define HMAC_SHA256(k, kl, d, dl, o) \ #define HMAC_SHA256(k, kl, d, dl, o) \
do { \ do { \
ret = Curl_hmacit(Curl_HMAC_SHA256, \ result = Curl_hmacit(Curl_HMAC_SHA256, \
(unsigned char *)k, \ (unsigned char *)k, \
kl, \ kl, \
(unsigned char *)d, \ (unsigned char *)d, \
dl, o); \ dl, o); \
if(ret) { \ if(result) { \
goto fail; \ goto fail; \
} \ } \
} while(0) } while(0)
#define TIMESTAMP_SIZE 17 #define TIMESTAMP_SIZE 17
@ -369,9 +369,109 @@ fail:
return ret; return ret;
} }
struct pair {
const char *p;
size_t len;
};
static int compare_func(const void *a, const void *b)
{
const struct pair *aa = a;
const struct pair *bb = b;
return strncmp(aa->p, bb->p, aa->len < bb->len ? aa->len : bb->len);
}
#define MAX_QUERYPAIRS 64
static CURLcode canon_query(struct Curl_easy *data,
const char *query, struct dynbuf *dq)
{
CURLcode result = CURLE_OK;
int entry = 0;
int i;
const char *p = query;
struct pair array[MAX_QUERYPAIRS];
struct pair *ap = &array[0];
if(!query)
return result;
/* sort the name=value pairs first */
do {
char *amp;
entry++;
ap->p = p;
amp = strchr(p, '&');
if(amp)
ap->len = amp - p; /* excluding the ampersand */
else {
ap->len = strlen(p);
break;
}
ap++;
p = amp + 1;
} while(entry < MAX_QUERYPAIRS);
if(entry == MAX_QUERYPAIRS) {
/* too many query pairs for us */
failf(data, "aws-sigv4: too many query pairs in URL");
return CURLE_URL_MALFORMAT;
}
qsort(&array[0], entry, sizeof(struct pair), compare_func);
ap = &array[0];
for(i = 0; !result && (i < entry); i++, ap++) {
size_t len;
const char *q = ap->p;
for(len = ap->len; len && !result; q++, len--) {
if(ISALNUM(*q))
result = Curl_dyn_addn(dq, q, 1);
else {
switch(*q) {
case '-':
case '.':
case '_':
case '~':
case '=':
/* allowed as-is */
result = Curl_dyn_addn(dq, q, 1);
break;
case '%':
/* uppercase the following if hexadecimal */
if(ISXDIGIT(q[1]) && ISXDIGIT(q[2])) {
char tmp[3]="%";
tmp[1] = Curl_raw_toupper(q[1]);
tmp[2] = Curl_raw_toupper(q[2]);
result = Curl_dyn_addn(dq, tmp, 3);
q += 2;
}
else
/* '%' without a following two-digit hex, encode it */
result = Curl_dyn_addn(dq, "%25", 3);
break;
default: {
/* URL encode */
const char hex[] = "0123456789ABCDEF";
char out[3]={'%'};
out[1] = hex[((unsigned char)*q)>>4];
out[2] = hex[*q & 0xf];
result = Curl_dyn_addn(dq, out, 3);
break;
}
}
}
}
if(i < entry - 1) {
/* insert ampersands between query pairs */
result = Curl_dyn_addn(dq, "&", 1);
}
}
return result;
}
CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy) CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
{ {
CURLcode ret = CURLE_OUT_OF_MEMORY; CURLcode result = CURLE_OUT_OF_MEMORY;
struct connectdata *conn = data->conn; struct connectdata *conn = data->conn;
size_t len; size_t len;
const char *arg; const char *arg;
@ -387,6 +487,7 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
char date[9]; char date[9];
struct dynbuf canonical_headers; struct dynbuf canonical_headers;
struct dynbuf signed_headers; struct dynbuf signed_headers;
struct dynbuf canonical_query;
char *date_header = NULL; char *date_header = NULL;
Curl_HttpReq httpreq; Curl_HttpReq httpreq;
const char *method = NULL; const char *method = NULL;
@ -415,6 +516,7 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
/* we init those buffers here, so goto fail will free initialized dynbuf */ /* we init those buffers here, so goto fail will free initialized dynbuf */
Curl_dyn_init(&canonical_headers, CURL_MAX_HTTP_HEADER); Curl_dyn_init(&canonical_headers, CURL_MAX_HTTP_HEADER);
Curl_dyn_init(&canonical_query, CURL_MAX_HTTP_HEADER);
Curl_dyn_init(&signed_headers, CURL_MAX_HTTP_HEADER); Curl_dyn_init(&signed_headers, CURL_MAX_HTTP_HEADER);
/* /*
@ -430,15 +532,15 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
/* provider1[:provider2[:region[:service]]] /* provider1[:provider2[:region[:service]]]
No string can be longer than N bytes of non-whitespace No string can be longer than N bytes of non-whitespace
*/ */
(void)sscanf(arg, "%" MAX_SIGV4_LEN_TXT "[^:]" (void)sscanf(arg, "%" MAX_SIGV4_LEN_TXT "[^:]"
":%" MAX_SIGV4_LEN_TXT "[^:]" ":%" MAX_SIGV4_LEN_TXT "[^:]"
":%" MAX_SIGV4_LEN_TXT "[^:]" ":%" MAX_SIGV4_LEN_TXT "[^:]"
":%" MAX_SIGV4_LEN_TXT "s", ":%" MAX_SIGV4_LEN_TXT "s",
provider0, provider1, region, service); provider0, provider1, region, service);
if(!provider0[0]) { if(!provider0[0]) {
failf(data, "first provider can't be empty"); failf(data, "first aws-sigv4 provider can't be empty");
ret = CURLE_BAD_FUNCTION_ARGUMENT; result = CURLE_BAD_FUNCTION_ARGUMENT;
goto fail; goto fail;
} }
else if(!provider1[0]) else if(!provider1[0])
@ -447,35 +549,38 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
if(!service[0]) { if(!service[0]) {
char *hostdot = strchr(hostname, '.'); char *hostdot = strchr(hostname, '.');
if(!hostdot) { if(!hostdot) {
failf(data, "service missing in parameters and hostname"); failf(data, "aws-sigv4: service missing in parameters and hostname");
ret = CURLE_URL_MALFORMAT; result = CURLE_URL_MALFORMAT;
goto fail; goto fail;
} }
len = hostdot - hostname; len = hostdot - hostname;
if(len > MAX_SIGV4_LEN) { if(len > MAX_SIGV4_LEN) {
failf(data, "service too long in hostname"); failf(data, "aws-sigv4: service too long in hostname");
ret = CURLE_URL_MALFORMAT; result = CURLE_URL_MALFORMAT;
goto fail; goto fail;
} }
strncpy(service, hostname, len); strncpy(service, hostname, len);
service[len] = '\0'; service[len] = '\0';
infof(data, "aws_sigv4: picked service %s from host", service);
if(!region[0]) { if(!region[0]) {
const char *reg = hostdot + 1; const char *reg = hostdot + 1;
const char *hostreg = strchr(reg, '.'); const char *hostreg = strchr(reg, '.');
if(!hostreg) { if(!hostreg) {
failf(data, "region missing in parameters and hostname"); failf(data, "aws-sigv4: region missing in parameters and hostname");
ret = CURLE_URL_MALFORMAT; result = CURLE_URL_MALFORMAT;
goto fail; goto fail;
} }
len = hostreg - reg; len = hostreg - reg;
if(len > MAX_SIGV4_LEN) { if(len > MAX_SIGV4_LEN) {
failf(data, "region too long in hostname"); failf(data, "aws-sigv4: region too long in hostname");
ret = CURLE_URL_MALFORMAT; result = CURLE_URL_MALFORMAT;
goto fail; goto fail;
} }
strncpy(region, reg, len); strncpy(region, reg, len);
region[len] = '\0'; region[len] = '\0';
infof(data, "aws_sigv4: picked region %s from host", region);
} }
} }
@ -490,11 +595,11 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
if(!payload_hash) { if(!payload_hash) {
if(sign_as_s3) if(sign_as_s3)
ret = calc_s3_payload_hash(data, httpreq, provider1, sha_hash, result = calc_s3_payload_hash(data, httpreq, provider1, sha_hash,
sha_hex, content_sha256_hdr); sha_hex, content_sha256_hdr);
else else
ret = calc_payload_hash(data, sha_hash, sha_hex); result = calc_payload_hash(data, sha_hash, sha_hex);
if(ret) if(result)
goto fail; goto fail;
payload_hash = sha_hex; payload_hash = sha_hex;
@ -513,21 +618,20 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
#else #else
time(&clock); time(&clock);
#endif #endif
ret = Curl_gmtime(clock, &tm); result = Curl_gmtime(clock, &tm);
if(ret) { if(result) {
goto fail; goto fail;
} }
if(!strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%SZ", &tm)) { if(!strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%SZ", &tm)) {
ret = CURLE_OUT_OF_MEMORY; result = CURLE_OUT_OF_MEMORY;
goto fail; goto fail;
} }
ret = make_headers(data, hostname, timestamp, provider1, result = make_headers(data, hostname, timestamp, provider1,
&date_header, content_sha256_hdr, &date_header, content_sha256_hdr,
&canonical_headers, &signed_headers); &canonical_headers, &signed_headers);
if(ret) if(result)
goto fail; goto fail;
ret = CURLE_OUT_OF_MEMORY;
if(*content_sha256_hdr) { if(*content_sha256_hdr) {
/* make_headers() needed this without the \r\n for canonicalization */ /* make_headers() needed this without the \r\n for canonicalization */
@ -539,6 +643,11 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
memcpy(date, timestamp, sizeof(date)); memcpy(date, timestamp, sizeof(date));
date[sizeof(date) - 1] = 0; date[sizeof(date) - 1] = 0;
result = canon_query(data, data->state.up.query, &canonical_query);
if(result)
goto fail;
result = CURLE_OUT_OF_MEMORY;
canonical_request = canonical_request =
curl_maprintf("%s\n" /* HTTPRequestMethod */ curl_maprintf("%s\n" /* HTTPRequestMethod */
"%s\n" /* CanonicalURI */ "%s\n" /* CanonicalURI */
@ -548,7 +657,8 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
"%.*s", /* HashedRequestPayload in hex */ "%.*s", /* HashedRequestPayload in hex */
method, method,
data->state.up.path, data->state.up.path,
data->state.up.query ? data->state.up.query : "", Curl_dyn_ptr(&canonical_query) ?
Curl_dyn_ptr(&canonical_query) : "",
Curl_dyn_ptr(&canonical_headers), Curl_dyn_ptr(&canonical_headers),
Curl_dyn_ptr(&signed_headers), Curl_dyn_ptr(&signed_headers),
(int)payload_hash_len, payload_hash); (int)payload_hash_len, payload_hash);
@ -632,9 +742,10 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
Curl_safefree(data->state.aptr.userpwd); Curl_safefree(data->state.aptr.userpwd);
data->state.aptr.userpwd = auth_headers; data->state.aptr.userpwd = auth_headers;
data->state.authhost.done = TRUE; data->state.authhost.done = TRUE;
ret = CURLE_OK; result = CURLE_OK;
fail: fail:
Curl_dyn_free(&canonical_query);
Curl_dyn_free(&canonical_headers); Curl_dyn_free(&canonical_headers);
Curl_dyn_free(&signed_headers); Curl_dyn_free(&signed_headers);
free(canonical_request); free(canonical_request);
@ -643,7 +754,7 @@ fail:
free(str_to_sign); free(str_to_sign);
free(secret); free(secret);
free(date_header); free(date_header);
return ret; return result;
} }
#endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) */ #endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) */