From 4d7bf5dcdab797f7da099a7c7127c753b3d31943 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 30 Jun 2020 19:32:17 -0700 Subject: [PATCH] Validate header field names. (#99) This rejects spaces after field names as well as various other invalid characters. Fixes #96 --- src/header.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/src/header.rs b/src/header.rs index 056a4b8..719ccb9 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,9 +2,15 @@ use crate::error::Error; use std::str::FromStr; #[derive(Clone)] -/// Wrapper type for a header line. +/// Wrapper type for a header field. +/// https://tools.ietf.org/html/rfc7230#section-3.2 pub struct Header { + // Line contains the unmodified bytes of single header field. + // It does not contain the final CRLF. line: String, + // Index is the position of the colon within the header field. + // Invariant: index > 0 + // Invariant: index + 1 < line.len() index: usize, } @@ -82,6 +88,29 @@ pub fn add_header(headers: &mut Vec
, header: Header) { headers.push(header); } +// https://tools.ietf.org/html/rfc7230#section-3.2.3 +// Each header field consists of a case-insensitive field name followed +// by a colon (":"), optional leading whitespace, the field value, and +// optional trailing whitespace. +// field-name = token +// token = 1*tchar +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +fn valid_name(name: &str) -> bool { + name.len() > 0 && name.bytes().all(is_tchar) +} + +#[inline] +fn is_tchar(b: u8) -> bool { + match b { + b'!' | b'#' | b'$' | b'%' | b'&' => true, + b'\'' | b'*' | b'+' | b'-' | b'.' => true, + b'^' | b'_' | b'`' | b'|' | b'~' => true, + b if b.is_ascii_alphanumeric() => true, + _ => false, + } +} + impl FromStr for Header { type Err = Error; fn from_str(s: &str) -> Result { @@ -94,16 +123,43 @@ impl FromStr for Header { return Err(Error::BadHeader); } - // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - // The field value MAY be preceded by any amount of LWS, though a single SP is preferred. - let value_from = &s[index..]; - let voff = match value_from.find(|c: char| !c.is_whitespace()) { - Some(n) => n, - None => 0, - }; - - let index = index + voff; + if !valid_name(&line[0..index]) { + return Err(Error::BadHeader); + } Ok(Header { line, index }) } } + +#[test] +fn test_valid_name() { + assert!(valid_name("example")); + assert!(valid_name("Content-Type")); + assert!(valid_name("h-123456789")); + assert!(!valid_name("Content-Type:")); + assert!(!valid_name("Content-Type ")); + assert!(!valid_name(" some-header")); + assert!(!valid_name("\"invalid\"")); + assert!(!valid_name("Gödel")); +} + +#[test] +fn test_parse_invalid_name() { + let h = "Content-Type :".parse::
(); + match h { + Err(Error::BadHeader) => {} + h => assert!(false, "expected BadHeader error, got {:?}", h), + } +} + +#[test] +fn empty_value() { + let h = "foo:".parse::
().unwrap(); + assert_eq!(h.value(), ""); +} + +#[test] +fn value_with_whitespace() { + let h = "foo: bar ".parse::
().unwrap(); + assert_eq!(h.value(), "bar"); +}