mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 08:14:11 +00:00
Reject whitespace-padded XHTTP client header values
HTTP/1 receivers strip leading and trailing whitespace from field values and HTTP/2 and HTTP/3 forbid it, so such a configured value is never sent intact and can never satisfy a server's matching_headers. Apply the server's existing rule to client headers through one shared value check.
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@
|
||||
- Updated Rust dependencies, including GotaTun 0.10.2, Tokio 1.54.0, LRU 0.19.0, and ML-KEM 0.3.3, which removes secret-dependent table lookups. Existing Web PKI compromise protections are retained.
|
||||
- Refreshed the BLAKE3 fork with upstream buffer-alignment and AVX-512 soundness fixes while retaining the binary-context key derivation API.
|
||||
- Updated Android and CI build dependencies; Android source builds now require Gradle 9.6.0 or newer with Java 17.
|
||||
- XHTTP server `matching_headers` values may now be lists. A request must still send exactly one value, which may match any listed value, so secrets can rotate without downtime. XHTTP client `headers` now reject names that differ only in case. Such configs previously sent every value, which a server with `matching_headers` always rejects.
|
||||
- XHTTP server `matching_headers` values may now be lists. A request must still send exactly one value, which may match any listed value, so secrets can rotate without downtime. XHTTP client `headers` now reject names that differ only in case, which previously sent every value and always failed a server's `matching_headers`, and values with leading or trailing whitespace, which receivers strip or reject.
|
||||
- Removed XHTTP server `close_on_http1_error`; configurations that still set it fail to load. HTTP/1 header and path mismatches now return the same plain 404 as HTTP/2 and HTTP/3. All XHTTP error responses now omit XHTTP padding, caching, and CORS headers, not only 400 and 404; successful responses are unchanged.
|
||||
|
||||
## v0.3.4
|
||||
|
||||
+27
-11
@@ -129,6 +129,23 @@ fn validate_header_names<'a>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_header_values<'a>(
|
||||
field: &str,
|
||||
values: impl IntoIterator<Item = &'a String>,
|
||||
) -> io::Result<()> {
|
||||
for value in values {
|
||||
http::HeaderValue::try_from(value).map_err(io::Error::other)?;
|
||||
// HTTP/1 receivers strip edge whitespace and HTTP/2 and HTTP/3 forbid it,
|
||||
// so such a value never arrives as configured.
|
||||
if value.starts_with([' ', '\t']) || value.ends_with([' ', '\t']) {
|
||||
return Err(invalid(&format!(
|
||||
"XHTTP {field} values cannot start or end with whitespace"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct XhttpLimits {
|
||||
@@ -339,9 +356,7 @@ impl XhttpClientConfig {
|
||||
return Err(invalid("direct cannot be an XHTTP inner protocol"));
|
||||
}
|
||||
validate_header_names("headers", self.headers.keys())?;
|
||||
for value in self.headers.values() {
|
||||
http::HeaderValue::try_from(value).map_err(io::Error::other)?;
|
||||
}
|
||||
validate_header_values("headers", self.headers.values())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -380,14 +395,10 @@ impl XhttpServerConfig {
|
||||
pub fn validate(&self) -> io::Result<()> {
|
||||
validate_path_and_host(&self.path, self.host.as_deref())?;
|
||||
validate_header_names("matching_headers", self.matching_headers.keys())?;
|
||||
for value in self.matching_headers.values().flat_map(OneOrSome::iter) {
|
||||
http::HeaderValue::try_from(value).map_err(io::Error::other)?;
|
||||
if value.starts_with([' ', '\t']) || value.ends_with([' ', '\t']) {
|
||||
return Err(invalid(
|
||||
"XHTTP matching header values cannot start or end with whitespace",
|
||||
));
|
||||
}
|
||||
}
|
||||
validate_header_values(
|
||||
"matching_headers",
|
||||
self.matching_headers.values().flat_map(OneOrSome::iter),
|
||||
)?;
|
||||
if contains_server(&self.protocol) {
|
||||
return Err(invalid("nested XHTTP is not supported"));
|
||||
}
|
||||
@@ -652,6 +663,11 @@ mod tests {
|
||||
(serde_json::json!({"x-token": "a\r\nx-injected: b"}), false),
|
||||
(serde_json::json!({"Host": "front.test"}), false),
|
||||
(serde_json::json!({"Connection": "keep-alive"}), false),
|
||||
(serde_json::json!({"x-token": "a b\tc"}), true),
|
||||
(serde_json::json!({"x-token": " a"}), false),
|
||||
(serde_json::json!({"x-token": "a "}), false),
|
||||
(serde_json::json!({"x-token": "\ta"}), false),
|
||||
(serde_json::json!({"x-token": "a\t"}), false),
|
||||
] {
|
||||
let config: XhttpClientConfig = serde_json::from_value(serde_json::json!({
|
||||
"headers": headers, "protocol": {"type": "socks"}
|
||||
|
||||
Reference in New Issue
Block a user