Skip to content

Commit 521fa8d

Browse files
ysaito1001rcohSaito
authored
Fix aws-sigv4 canonical request formatting fallibility (#1656)
* Fix panic occurred in formatting CanonicalRequest This commit addresses panic in the case of formatting a CanonicalRequest containing invalid UTF-8 in the header value. The commit aims for the least disturbance to the codebase in order to pass a given failing test in the PR. We want to quickly determine if this low-cost approach addresses the problem before we commit ourselves to start refactoring CanonicalRequest to have a special format function. Fixes #711 * Update test_signing_utf8_headers to proptest This commit converts test_signing_utf8_headers to a proptest. The original test only specified hardcoded non-UTF8 bytes in a request header. To ensure that `crate::http_request::sign` does not panic no matter what valid byte sequence for HeaderValue is, the proptest covers a wider range of inputs. For consistency, the test has been moved from `canonical_request.rs` to `sign.rs`. * Add InvalidHeaderError to make the error explicit This commit introduces an error type InvalidHeaderError to indicate that we ran into a problem in handling a HeaderValue within CanonicalRequest. The error type contains a source error such as Utf8Error so a diagnostic message can be printed if needed. * Remove InvalidHeaderError for error refactoring This commit effectively reverts 739b32c. Knowing that we will be cleaning up error types, having InvalidHeaderError is too narrow a solution and does not add value to the codebase. * Update CHANGELOG.next.toml Co-authored-by: Russell Cohen <rcoh@amazon.com> Co-authored-by: Saito <awsaito@c889f3b5ddc4.ant.amazon.com>
1 parent 374a0a2 commit 521fa8d

File tree

3 files changed

+76
-7
lines changed

3 files changed

+76
-7
lines changed

CHANGELOG.next.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,9 @@ message = "Ability to override the IMDS client in `DefaultCredentialsChain`"
186186
references = ["aws-sdk-rust#625"]
187187
meta = { "breaking" = false, "tada" = false, "bug" = false }
188188
author = "kevinpark1217"
189+
190+
[[aws-sdk-rust]]
191+
message = "Fix aws-sigv4 canonical request formatting fallibility."
192+
references = ["smithy-rs#1656"]
193+
meta = { "breaking" = false, "tada" = false, "bug" = true }
194+
author = "ysaito1001"

aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ impl<'a> CanonicalRequest<'a> {
196196
// Using append instead of insert means this will not clobber headers that have the same lowercased name
197197
canonical_headers.append(
198198
HeaderName::from_str(&name.as_str().to_lowercase())?,
199-
normalize_header_value(value),
199+
normalize_header_value(value)?,
200200
);
201201
}
202202

@@ -373,11 +373,11 @@ fn trim_spaces_from_byte_string(bytes: &[u8]) -> &[u8] {
373373
&bytes[starting_index..ending_index]
374374
}
375375

376-
/// Works just like [trim_all] but acts on HeaderValues instead of bytes
377-
fn normalize_header_value(header_value: &HeaderValue) -> HeaderValue {
376+
/// Works just like [trim_all] but acts on HeaderValues instead of bytes.
377+
/// Will ensure that the underlying bytes are valid UTF-8.
378+
fn normalize_header_value(header_value: &HeaderValue) -> Result<HeaderValue, Error> {
378379
let trimmed_value = trim_all(header_value.as_bytes());
379-
// This can't fail because we started with a valid HeaderValue and then only trimmed spaces
380-
HeaderValue::from_bytes(&trimmed_value).unwrap()
380+
HeaderValue::from_str(std::str::from_utf8(&trimmed_value)?).map_err(Error::from)
381381
}
382382

383383
#[derive(Debug, PartialEq, Default)]
@@ -738,9 +738,9 @@ mod tests {
738738
}
739739

740740
#[test]
741-
fn test_normalize_header_value_doesnt_panic(v in (".*")) {
741+
fn test_normalize_header_value_works_on_valid_header_value(v in (".*")) {
742742
if let Ok(header_value) = HeaderValue::from_maybe_shared(v) {
743-
let _ = normalize_header_value(&header_value);
743+
assert!(normalize_header_value(&header_value).is_ok());
744744
}
745745
}
746746

@@ -749,4 +749,10 @@ mod tests {
749749
assert_eq!(trim_all(s.as_bytes()).as_ref(), s.as_bytes());
750750
}
751751
}
752+
753+
#[test]
754+
fn test_normalize_header_value_returns_expected_error_on_invalid_utf8() {
755+
let header_value = HeaderValue::from_bytes(&[0xC0, 0xC1]).unwrap();
756+
assert!(normalize_header_value(&header_value).is_err());
757+
}
752758
}

aws/rust-runtime/aws-sigv4/src/http_request/sign.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ mod tests {
311311
use crate::http_request::{SignatureLocation, SigningParams, SigningSettings};
312312
use http::{HeaderMap, HeaderValue};
313313
use pretty_assertions::assert_eq;
314+
use proptest::proptest;
314315
use std::borrow::Cow;
315316
use std::time::Duration;
316317

@@ -515,6 +516,62 @@ mod tests {
515516
assert_req_eq!(expected, signed);
516517
}
517518

519+
#[test]
520+
fn test_sign_headers_returning_expected_error_on_invalid_utf8() {
521+
let settings = SigningSettings::default();
522+
let params = SigningParams {
523+
access_key: "123",
524+
secret_key: "asdf",
525+
security_token: None,
526+
region: "us-east-1",
527+
service_name: "foo",
528+
time: std::time::SystemTime::now(),
529+
settings,
530+
};
531+
532+
let req = http::Request::builder()
533+
.uri("https://foo.com/")
534+
.header("x-sign-me", HeaderValue::from_bytes(&[0xC0, 0xC1]).unwrap())
535+
.body(&[])
536+
.unwrap();
537+
538+
let creq = crate::http_request::sign(SignableRequest::from(&req), &params);
539+
assert!(creq.is_err());
540+
}
541+
542+
proptest! {
543+
#[test]
544+
// Only byte values between 32 and 255 (inclusive) are permitted, excluding byte 127, for
545+
// [HeaderValue](https://docs.rs/http/latest/http/header/struct.HeaderValue.html#method.from_bytes).
546+
fn test_sign_headers_no_panic(
547+
left in proptest::collection::vec(32_u8..=126, 0..100),
548+
right in proptest::collection::vec(128_u8..=255, 0..100),
549+
) {
550+
let settings = SigningSettings::default();
551+
let params = SigningParams {
552+
access_key: "123",
553+
secret_key: "asdf",
554+
security_token: None,
555+
region: "us-east-1",
556+
service_name: "foo",
557+
time: std::time::SystemTime::now(),
558+
settings,
559+
};
560+
561+
let bytes = left.iter().chain(right.iter()).cloned().collect::<Vec<_>>();
562+
let req = http::Request::builder()
563+
.uri("https://foo.com/")
564+
.header("x-sign-me", HeaderValue::from_bytes(&bytes).unwrap())
565+
.body(&[])
566+
.unwrap();
567+
568+
// The test considered a pass if the creation of `creq` does not panic.
569+
let _creq = crate::http_request::sign(
570+
SignableRequest::from(&req),
571+
&params);
572+
}
573+
}
574+
518575
#[test]
519576
fn apply_signing_instructions_headers() {
520577
let mut headers = HeaderMap::new();

0 commit comments

Comments
 (0)