Skip to content

Commit 9fed0e3

Browse files
committed
fix(mmds): Validate X-metadata-token-ttl-seconds header only if needed
We checked that "X-metadata-token-ttl-seconds" has a valid positive number regardless of the request type. On the oher hand, EC2 IMDS only validates it only if it is a PUT request to "/latest/api/token". Such a behavioral difference from EC2 IMDS could result in unexpected issues. Signed-off-by: Takahiro Itazuri <[email protected]>
1 parent c6f9946 commit 9fed0e3

File tree

3 files changed

+148
-218
lines changed

3 files changed

+148
-218
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ and this project adheres to
2828
Incremental snapshots remain in developer preview.
2929
- [#5282](https://github.com/firecracker-microvm/firecracker/pull/5282): Updated
3030
jailer to no longer require the executable file name to contain `firecracker`.
31+
- [#5290](https://github.com/firecracker-microvm/firecracker/pull/5290): Changed
32+
MMDS to validate the value of "X-metadata-token-ttl-seconds" header only if it
33+
is a PUT request to /latest/api/token, as in EC2 IMDS.
3134

3235
### Deprecated
3336

src/vmm/src/mmds/mod.rs

Lines changed: 88 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ use micro_http::{
1717
Body, HttpHeaderError, MediaType, Method, Request, RequestError, Response, StatusCode, Version,
1818
};
1919
use serde_json::{Map, Value};
20-
use token_headers::TokenHeaders;
2120

2221
use crate::mmds::data_store::{Mmds, MmdsDatastoreError as MmdsError, MmdsVersion, OutputFormat};
2322
use crate::mmds::token::PATH_TO_TOKEN;
24-
use crate::mmds::token_headers::REJECTED_HEADER;
23+
use crate::mmds::token_headers::{
24+
REJECTED_HEADER, X_METADATA_TOKEN_HEADER, X_METADATA_TOKEN_TTL_SECONDS_HEADER,
25+
get_header_value_pair,
26+
};
2527

2628
#[rustfmt::skip]
2729
#[derive(Debug, thiserror::Error, displaydoc::Display)]
@@ -142,23 +144,10 @@ fn respond_to_request_mmdsv1(mmds: &Mmds, request: Request) -> Response {
142144
}
143145

144146
fn respond_to_request_mmdsv2(mmds: &mut Mmds, request: Request) -> Response {
145-
// Fetch custom headers from request.
146-
let token_headers = match TokenHeaders::try_from(request.headers.custom_entries()) {
147-
Ok(token_headers) => token_headers,
148-
Err(err) => {
149-
return build_response(
150-
request.http_version(),
151-
StatusCode::BadRequest,
152-
MediaType::PlainText,
153-
Body::new(err.to_string()),
154-
);
155-
}
156-
};
157-
158147
// Allow only GET and PUT requests.
159148
match request.method() {
160-
Method::Get => respond_to_get_request_checked(mmds, request, token_headers),
161-
Method::Put => respond_to_put_request(mmds, request, token_headers),
149+
Method::Get => respond_to_get_request_checked(mmds, request),
150+
Method::Put => respond_to_put_request(mmds, request),
162151
_ => {
163152
let mut response = build_response(
164153
request.http_version(),
@@ -173,26 +162,23 @@ fn respond_to_request_mmdsv2(mmds: &mut Mmds, request: Request) -> Response {
173162
}
174163
}
175164

176-
fn respond_to_get_request_checked(
177-
mmds: &Mmds,
178-
request: Request,
179-
token_headers: TokenHeaders,
180-
) -> Response {
181-
// Get MMDS token from custom headers.
182-
let token = match token_headers.x_metadata_token() {
183-
Some(token) => token,
184-
None => {
185-
let error_msg = VmmMmdsError::NoTokenProvided.to_string();
186-
return build_response(
187-
request.http_version(),
188-
StatusCode::Unauthorized,
189-
MediaType::PlainText,
190-
Body::new(error_msg),
191-
);
192-
}
193-
};
165+
fn respond_to_get_request_checked(mmds: &Mmds, request: Request) -> Response {
166+
// Check whether a token exists.
167+
let token =
168+
match get_header_value_pair(request.headers.custom_entries(), X_METADATA_TOKEN_HEADER) {
169+
Some((_, token)) => token,
170+
None => {
171+
let error_msg = VmmMmdsError::NoTokenProvided.to_string();
172+
return build_response(
173+
request.http_version(),
174+
StatusCode::Unauthorized,
175+
MediaType::PlainText,
176+
Body::new(error_msg),
177+
);
178+
}
179+
};
194180

195-
// Validate MMDS token.
181+
// Validate the token.
196182
match mmds.is_valid_token(token) {
197183
Ok(true) => respond_to_get_request_unchecked(mmds, request),
198184
Ok(false) => build_response(
@@ -248,17 +234,11 @@ fn respond_to_get_request_unchecked(mmds: &Mmds, request: Request) -> Response {
248234
}
249235
}
250236

251-
fn respond_to_put_request(
252-
mmds: &mut Mmds,
253-
request: Request,
254-
token_headers: TokenHeaders,
255-
) -> Response {
237+
fn respond_to_put_request(mmds: &mut Mmds, request: Request) -> Response {
238+
let custom_headers = request.headers.custom_entries();
239+
256240
// Reject `PUT` requests that contain `X-Forwarded-For` header.
257-
if request
258-
.headers
259-
.custom_entries()
260-
.contains_key(REJECTED_HEADER)
261-
{
241+
if custom_headers.contains_key(REJECTED_HEADER) {
262242
let error_msg = RequestError::HeaderError(HttpHeaderError::UnsupportedName(
263243
REJECTED_HEADER.to_string(),
264244
))
@@ -287,17 +267,36 @@ fn respond_to_put_request(
287267
}
288268

289269
// Get token lifetime value.
290-
let ttl_seconds = match token_headers.x_metadata_token_ttl_seconds() {
291-
Some(ttl_seconds) => ttl_seconds,
292-
None => {
293-
return build_response(
294-
request.http_version(),
295-
StatusCode::BadRequest,
296-
MediaType::PlainText,
297-
Body::new(VmmMmdsError::NoTtlProvided.to_string()),
298-
);
299-
}
300-
};
270+
let ttl_seconds =
271+
match get_header_value_pair(custom_headers, X_METADATA_TOKEN_TTL_SECONDS_HEADER) {
272+
// Header found
273+
Some((k, v)) => match v.parse::<u32>() {
274+
Ok(ttl_seconds) => ttl_seconds,
275+
Err(_) => {
276+
return build_response(
277+
request.http_version(),
278+
StatusCode::BadRequest,
279+
MediaType::PlainText,
280+
Body::new(
281+
RequestError::HeaderError(HttpHeaderError::InvalidValue(
282+
k.into(),
283+
v.into(),
284+
))
285+
.to_string(),
286+
),
287+
);
288+
}
289+
},
290+
// Header not found
291+
None => {
292+
return build_response(
293+
request.http_version(),
294+
StatusCode::BadRequest,
295+
MediaType::PlainText,
296+
Body::new(VmmMmdsError::NoTtlProvided.to_string()),
297+
);
298+
}
299+
};
301300

302301
// Generate token.
303302
let result = mmds.generate_token(ttl_seconds);
@@ -530,7 +529,7 @@ mod tests {
530529
let actual_response = convert_to_response(mmds.clone(), request);
531530
assert_eq!(actual_response, expected_response);
532531

533-
// Test invalid custom header value is ignored when V1 is configured.
532+
// Test invalid custom header value is ignored if not PUT request to /latest/api/token.
534533
let (request, expected_response) = generate_request_and_expected_response(
535534
b"GET http://169.254.169.254/ HTTP/1.0\r\n\
536535
Accept: application/json\r\n\
@@ -576,23 +575,6 @@ mod tests {
576575
let actual_response = convert_to_response(mmds.clone(), request);
577576
assert_eq!(actual_response, expected_response);
578577

579-
// Test invalid value for custom header.
580-
let request = Request::try_from(
581-
b"GET http://169.254.169.254/ HTTP/1.0\r\n\
582-
Accept: application/json\r\n\
583-
X-metadata-token-ttl-seconds: -60\r\n\r\n",
584-
None,
585-
)
586-
.unwrap();
587-
let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest);
588-
expected_response.set_content_type(MediaType::PlainText);
589-
expected_response.set_body(Body::new(
590-
"Invalid header. Reason: Invalid value. Key:X-metadata-token-ttl-seconds; Value:-60"
591-
.to_string(),
592-
));
593-
let actual_response = convert_to_response(mmds.clone(), request);
594-
assert_eq!(actual_response, expected_response);
595-
596578
// Test PUT requests.
597579
// Unsupported `X-Forwarded-For` header present.
598580
let request = Request::try_from(
@@ -624,6 +606,22 @@ mod tests {
624606
let actual_response = convert_to_response(mmds.clone(), request);
625607
assert_eq!(actual_response, expected_response);
626608

609+
// Test invalid X-metadata-token-ttl-seconds value gets BadRequest.
610+
let request = Request::try_from(
611+
b"PUT http://169.254.169.254/latest/api/token HTTP/1.0\r\n\
612+
X-metadata-token-ttl-seconds: -60\r\n\r\n",
613+
None,
614+
)
615+
.unwrap();
616+
let mut expected_response = Response::new(Version::Http10, StatusCode::BadRequest);
617+
expected_response.set_content_type(MediaType::PlainText);
618+
expected_response.set_body(Body::new(
619+
"Invalid header. Reason: Invalid value. Key:X-metadata-token-ttl-seconds; Value:-60"
620+
.to_string(),
621+
));
622+
let actual_response = convert_to_response(mmds.clone(), request);
623+
assert_eq!(actual_response, expected_response);
624+
627625
// Test invalid lifetime values for token.
628626
let invalid_values = [MIN_TOKEN_TTL_SECONDS - 1, MAX_TOKEN_TTL_SECONDS + 1];
629627
for invalid_value in invalid_values.iter() {
@@ -687,6 +685,21 @@ mod tests {
687685
let actual_response = convert_to_response(mmds.clone(), request);
688686
assert_eq!(actual_response, expected_response);
689687

688+
// Test invalid customer header value is ignored if not PUT request to /latest/api/token.
689+
#[rustfmt::skip]
690+
let (request, expected_response) = generate_request_and_expected_response(
691+
format!(
692+
"GET http://169.254.169.254/ HTTP/1.0\r\n\
693+
Accept: application/json\r\n\
694+
X-metadata-token: {valid_token}\r\n\
695+
X-metadata-token-ttl-seconds: -60\r\n\r\n",
696+
)
697+
.as_bytes(),
698+
MediaType::ApplicationJson,
699+
);
700+
let actual_response = convert_to_response(mmds.clone(), request);
701+
assert_eq!(actual_response, expected_response);
702+
690703
// Test GET request towards unsupported value type.
691704
#[rustfmt::skip]
692705
let request = Request::try_from(

0 commit comments

Comments
 (0)