--- /dev/null
+/* Copyright (C) 2021 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use std;
+use std::collections::HashMap;
+
+use nom::combinator::rest;
+use nom::error::ErrorKind;
+use nom::Err;
+use nom::IResult;
+
+#[derive(Clone)]
+pub struct MIMEHeaderTokens<'a> {
+ pub tokens: HashMap<&'a [u8], &'a [u8]>,
+}
+
+pub fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> {
+ let (i2, _) = tag!(input, "\"")?;
+ let (i3, value) = take_until!(i2, "\"")?;
+ let (i4, _) = tag!(i3, "\"")?;
+ return Ok((i4, value));
+}
+
+pub fn mime_parse_header_token(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
+ // from RFC2047 : like ch.is_ascii_whitespace but without 0x0c FORM-FEED
+ let (i1, _) = take_while!(input, |ch: u8| ch == 0x20
+ || ch == 0x09
+ || ch == 0x0a
+ || ch == 0x0d)?;
+ let (i2, name) = take_until!(i1, "=")?;
+ let (i3, _) = tag!(i2, "=")?;
+ let (i4, value) = alt!(
+ i3,
+ mime_parse_value_delimited | complete!(take_until!(";")) | rest
+ )?;
+ let (i5, _) = opt!(i4, complete!(tag!(";")))?;
+ return Ok((i5, (name, value)));
+}
+
+fn mime_parse_header_tokens(input: &[u8]) -> IResult<&[u8], MIMEHeaderTokens> {
+ let (mut i2, _) = take_until_and_consume!(input, ";")?;
+ let mut tokens = HashMap::new();
+ while i2.len() > 0 {
+ match mime_parse_header_token(i2) {
+ Ok((rem, t)) => {
+ tokens.insert(t.0, t.1);
+ // should never happen
+ debug_validate_bug_on!(i2.len() == rem.len());
+ if i2.len() == rem.len() {
+ //infinite loop
+ return Err(Err::Error((input, ErrorKind::Eof)));
+ }
+ i2 = rem;
+ }
+ Err(_) => {
+ // keep first tokens is error in remaining buffer
+ break;
+ }
+ }
+ }
+ return Ok((i2, MIMEHeaderTokens { tokens }));
+}
+
+fn mime_find_header_token<'a>(header: &'a [u8], token: &[u8]) -> Result<&'a [u8], ()> {
+ match mime_parse_header_tokens(header) {
+ Ok((_rem, t)) => {
+ // look for the specific token
+ match t.tokens.get(token) {
+ // easy nominal case
+ Some(value) => return Ok(value),
+ None => return Err(()),
+ }
+ }
+ Err(_) => {
+ return Err(());
+ }
+ }
+}
+
+// TODO ? export with "constants" in cbindgen
+// and use in outbuf definition for rs_mime_find_header_token
+// but other constants are now defined twice in rust and in C
+pub const RS_MIME_MAX_TOKEN_LEN: usize = 255;
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_mime_find_header_token(
+ hinput: *const u8, hlen: u32, tinput: *const u8, tlen: u32, outbuf: &mut [u8; 255],
+ outlen: *mut u32,
+) -> bool {
+ let hbuf = build_slice!(hinput, hlen as usize);
+ let tbuf = build_slice!(tinput, tlen as usize);
+ match mime_find_header_token(hbuf, tbuf) {
+ Ok(value) => {
+ // limit the copy to the supplied buffer size
+ if value.len() <= RS_MIME_MAX_TOKEN_LEN {
+ outbuf[..value.len()].clone_from_slice(value);
+ } else {
+ outbuf.clone_from_slice(&value[..RS_MIME_MAX_TOKEN_LEN]);
+ }
+ *outlen = value.len() as u32;
+ return true;
+ }
+ _ => {}
+ }
+ return false;
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_mime_find_header_token() {
+ let undelimok = mime_find_header_token(
+ "attachment; filename=test;".as_bytes(),
+ "filename".as_bytes(),
+ );
+ assert_eq!(undelimok, Ok("test".as_bytes()));
+
+ let delimok = mime_find_header_token(
+ "attachment; filename=\"test2\";".as_bytes(),
+ "filename".as_bytes(),
+ );
+ assert_eq!(delimok, Ok("test2".as_bytes()));
+
+ let evasion_othertoken = mime_find_header_token(
+ "attachment; dummy=\"filename=wrong\"; filename=real;".as_bytes(),
+ "filename".as_bytes(),
+ );
+ assert_eq!(evasion_othertoken, Ok("real".as_bytes()));
+
+ let evasion_suffixtoken = mime_find_header_token(
+ "attachment; notafilename=wrong; filename=good;".as_bytes(),
+ "filename".as_bytes(),
+ &mut outvec,
+ );
+ assert_eq!(evasion_suffixtoken, Ok("good".as_bytes()));
+
+ let badending = mime_find_header_token(
+ "attachment; filename=oksofar; badending".as_bytes(),
+ "filename".as_bytes(),
+ );
+ assert_eq!(badending, Ok("oksofar".as_bytes()));
+
+ let missend = mime_find_header_token(
+ "attachment; filename=test".as_bytes(),
+ "filename".as_bytes(),
+ );
+ assert_eq!(missend, Ok("test".as_bytes()));
+ }
+}
#define CTNT_DISP_STR "content-disposition"
#define CTNT_TRAN_STR "content-transfer-encoding"
#define MSG_ID_STR "message-id"
-#define BND_START_STR "boundary="
-#define TOK_END_STR "\""
#define MSG_STR "message/"
#define MULTIPART_STR "multipart/"
#define QP_STR "quoted-printable"
return ret;
}
-/**
- * \brief Finds a mime header token within the specified field
- *
- * \param field The current field
- * \param search_start The start of the search (ie. boundary=\")
- * \param search_end The end of the search (ie. \")
- * \param tlen The output length of the token (if found)
- * \param max_len The maximum offset in which to search
- * \param toolong Set if the field value was truncated to max_len.
- *
- * \return A pointer to the token if found, otherwise NULL if not found
- */
-static uint8_t * FindMimeHeaderTokenRestrict(MimeDecField *field, const char *search_start,
- const char *search_end, uint32_t *tlen, uint32_t max_len, bool *toolong)
-{
- uint8_t *fptr, *tptr = NULL, *tok = NULL;
-
- if (toolong)
- *toolong = false;
-
- SCLogDebug("Looking for token: %s", search_start);
-
- /* Check for token definition */
- size_t ss_len = strlen(search_start);
- fptr = FindBuffer(field->value, field->value_len, (const uint8_t *)search_start, ss_len);
- if (fptr != NULL) {
- fptr += ss_len; /* Start at end of start string */
- uint32_t offset = fptr - field->value;
- if (offset > field->value_len) {
- return tok;
- }
- tok = GetToken(fptr, field->value_len - offset, search_end, &tptr, tlen);
- if (tok == NULL) {
- return tok;
- }
- SCLogDebug("Found mime token");
-
- /* Compare the actual token length against the maximum */
- if (toolong && max_len && *tlen > max_len) {
- SCLogDebug("Token length %d exceeds length restriction %d; truncating", *tlen, max_len);
- *toolong = true;
- *tlen = max_len;
- }
- }
-
- return tok;
-}
-
-/**
- * \brief Finds a mime header token within the specified field
- *
- * \param field The current field
- * \param search_start The start of the search (ie. boundary=\")
- * \param search_end The end of the search (ie. \")
- * \param tlen The output length of the token (if found)
- *
- * \return A pointer to the token if found, otherwise NULL if not found
- */
-static uint8_t * FindMimeHeaderToken(MimeDecField *field, const char *search_start,
- const char *search_end, uint32_t *tlen)
-{
- return FindMimeHeaderTokenRestrict(field, search_start, search_end, tlen, 0, NULL);
-}
-
/**
* \brief Processes the current line for mime headers and also does post-processing
* when all headers found
{
int ret = MIME_DEC_OK;
MimeDecField *field;
- uint8_t *bptr = NULL, *rptr = NULL;
+ uint8_t *rptr = NULL;
uint32_t blen = 0;
MimeDecEntity *entity = (MimeDecEntity *) state->stack->top->data;
+ uint8_t bptr[NAME_MAX];
/* Look for mime header in current line */
ret = FindMimeHeader(buf, len, state);
field = MimeDecFindField(entity, CTNT_DISP_STR);
if (field != NULL) {
bool truncated_name = false;
- bptr = FindMimeHeaderTokenRestrict(field, "filename=", TOK_END_STR, &blen, NAME_MAX, &truncated_name);
- if (bptr != NULL) {
+ // NAME_MAX is RS_MIME_MAX_TOKEN_LEN on the rust side
+ if (rs_mime_find_header_token(field->value, field->value_len,
+ (const uint8_t *)"filename", strlen("filename"), &bptr, &blen)) {
SCLogDebug("File attachment found in disposition");
entity->ctnt_flags |= CTNT_IS_ATTACHMENT;
+ if (blen > NAME_MAX) {
+ blen = NAME_MAX;
+ truncated_name = true;
+ }
+
/* Copy over using dynamic memory */
entity->filename = SCMalloc(blen);
if (unlikely(entity->filename == NULL)) {
field = MimeDecFindField(entity, CTNT_TYPE_STR);
if (field != NULL) {
/* Check if child entity boundary definition found */
- bptr = FindMimeHeaderToken(field, BND_START_STR, TOK_END_STR, &blen);
- if (bptr != NULL) {
+ // NAME_MAX is RS_MIME_MAX_TOKEN_LEN on the rust side
+ if (rs_mime_find_header_token(field->value, field->value_len,
+ (const uint8_t *)"boundary", strlen("boundary"), &bptr, &blen)) {
state->found_child = 1;
entity->ctnt_flags |= CTNT_IS_MULTIPART;
/* Look for file name (if not already found) */
if (!(entity->ctnt_flags & CTNT_IS_ATTACHMENT)) {
bool truncated_name = false;
- bptr = FindMimeHeaderTokenRestrict(field, "name=", TOK_END_STR, &blen, NAME_MAX, &truncated_name);
- if (bptr != NULL) {
+ // NAME_MAX is RS_MIME_MAX_TOKEN_LEN on the rust side
+ if (rs_mime_find_header_token(field->value, field->value_len,
+ (const uint8_t *)"name", strlen("name"), &bptr, &blen)) {
SCLogDebug("File attachment found");
entity->ctnt_flags |= CTNT_IS_ATTACHMENT;
+ if (blen > NAME_MAX) {
+ blen = NAME_MAX;
+ truncated_name = true;
+ }
+
/* Copy over using dynamic memory */
entity->filename = SCMalloc(blen);
if (unlikely(entity->filename == NULL)) {