Explorar el Código

[ZF2015-04] Fix CRLF injections in HTTP and Mail

This patch mirrors that made in ZF2 to address ZF2015-04. It adds the following
classes:

- `Zend_Http_Header_HeaderValue`, which provides functionality for validating,
  filtering, and asserting that header values follow RFC 2822.
- `Zend_Mail_Header_HeaderName`, which provides functionality for validating,
  filtering, and asserting that header names follow RFC 2822.
- `Zend_Mail_Header_HeaderValue`, which provides functionality for validating,
  filtering, and asserting that header values follow RFC 7230.

The following specific changes were made to existing functionality:

- `Zend_Mail_Part::__construct()` was modified in order to validate mail headers
  provided to it.
- `Zend_Http_Header_SetCookie`'s `setName()`, `setValue()`, `setDomain()`, and
  `setPath()` methods were modified to validate incoming values.
- `Zend_Http_Response::extractHeaders()` was modified to follow RFC 7230 and
  only split on `\r\n` sequences when splitting header lines. Each value
  extracted is tested for validity.
- `Zend_Http_Response::extractBody()` was modified to follow RFC 7230 and
  only split on `\r\n` sequences when splitting the message from the headers.
- `Zend_Http_Client::setHeaders()` was modified to validate incoming header
  values.
Matthew Weier O'Phinney hace 10 años
padre
commit
b0490b41a3

+ 56 - 25
library/Zend/Http/Client.php

@@ -40,6 +40,12 @@ require_once 'Zend/Http/Client/Adapter/Interface.php';
 
 
 /**
+ * @see Zend_Http_Header_HeaderValue
+ */
+require_once 'Zend/Http/Header/HeaderValue.php';
+
+
+/**
  * @see Zend_Http_Response
  */
 require_once 'Zend/Http/Response.php';
@@ -431,38 +437,40 @@ class Zend_Http_Client
             foreach ($name as $k => $v) {
                 if (is_string($k)) {
                     $this->setHeaders($k, $v);
-                } else {
-                    $this->setHeaders($v, null);
+                    continue;
                 }
+                $this->setHeaders($v, null);
             }
-        } else {
-            // Check if $name needs to be split
-            if ($value === null && (strpos($name, ':') > 0)) {
-                list($name, $value) = explode(':', $name, 2);
-            }
+            return $this;
+        }
 
-            // Make sure the name is valid if we are in strict mode
-            if ($this->config['strict'] && (! preg_match('/^[a-zA-Z0-9-]+$/', $name))) {
-                /** @see Zend_Http_Client_Exception */
-                require_once 'Zend/Http/Client/Exception.php';
-                throw new Zend_Http_Client_Exception("{$name} is not a valid HTTP header name");
-            }
+        // Check if $name needs to be split
+        if ($value === null && (strpos($name, ':') > 0)) {
+            list($name, $value) = explode(':', $name, 2);
+        }
 
-            $normalized_name = strtolower($name);
+        // Make sure the name is valid if we are in strict mode
+        if ($this->config['strict'] && (! preg_match('/^[a-zA-Z0-9-]+$/', $name))) {
+            require_once 'Zend/Http/Client/Exception.php';
+            throw new Zend_Http_Client_Exception("{$name} is not a valid HTTP header name");
+        }
 
-            // If $value is null or false, unset the header
-            if ($value === null || $value === false) {
-                unset($this->headers[$normalized_name]);
+        $normalized_name = strtolower($name);
 
-            // Else, set the header
-            } else {
-                // Header names are stored lowercase internally.
-                if (is_string($value)) {
-                    $value = trim($value);
-                }
-                $this->headers[$normalized_name] = array($name, $value);
-            }
+        // If $value is null or false, unset the header
+        if ($value === null || $value === false) {
+            unset($this->headers[$normalized_name]);
+            return $this;
+        }
+
+        // Validate value
+        $this->_validateHeaderValue($value);
+
+        // Header names are stored lowercase internally.
+        if (is_string($value)) {
+            $value = trim($value);
         }
+        $this->headers[$normalized_name] = array($name, $value);
 
         return $this;
     }
@@ -1568,4 +1576,27 @@ class Zend_Http_Client
         return $parameters;
     }
 
+    /**
+     * Ensure a header value is valid per RFC 7230.
+     *
+     * @see http://tools.ietf.org/html/rfc7230#section-3.2
+     * @param string|object|array $value
+     * @param bool $recurse
+     */
+    protected function _validateHeaderValue($value, $recurse = true)
+    {
+        if (is_array($value) && $recurse) {
+            foreach ($value as $v) {
+                $this->_validateHeaderValue($v, false);
+            }
+            return;
+        }
+
+        if (! is_string($value) && (! is_object($value) || ! method_exists($value, '__toString'))) {
+            require_once 'Zend/Http/Exception.php';
+            throw new Zend_Http_Exception('Invalid header value detected');
+        }
+
+        Zend_Http_Header_HeaderValue::assertValid($value);
+    }
 }

+ 127 - 0
library/Zend/Http/Header/HeaderValue.php

@@ -0,0 +1,127 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Header
+ * @version    $Id$
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Header
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+final class Zend_Http_Header_HeaderValue
+{
+    /**
+     * Private constructor; non-instantiable.
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * Filter a header value
+     *
+     * Ensures CRLF header injection vectors are filtered.
+     *
+     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
+     * tabs are allowed in values; only one whitespace character is allowed
+     * between visible characters.
+     *
+     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
+     * @param string $value
+     * @return string
+     */
+    public static function filter($value)
+    {
+        $value  = (string) $value;
+        $length = strlen($value);
+        $string = '';
+        for ($i = 0; $i < $length; $i += 1) {
+            $ascii = ord($value[$i]);
+
+            // Non-visible, non-whitespace characters
+            // 9 === horizontal tab
+            // 32-126, 128-254 === visible
+            // 127 === DEL
+            // 255 === null byte
+            if (($ascii < 32 && $ascii !== 9)
+                || $ascii === 127
+                || $ascii > 254
+            ) {
+                continue;
+            }
+
+            $string .= $value[$i];
+        }
+
+        return $string;
+    }
+
+    /**
+     * Validate a header value.
+     *
+     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
+     * tabs are allowed in values; only one whitespace character is allowed
+     * between visible characters.
+     *
+     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
+     * @param string $value
+     * @return bool
+     */
+    public static function isValid($value)
+    {
+        $value  = (string) $value;
+        $length = strlen($value);
+        for ($i = 0; $i < $length; $i += 1) {
+            $ascii = ord($value[$i]);
+
+            // Non-visible, non-whitespace characters
+            // 9 === horizontal tab
+            // 32-126, 128-254 === visible
+            // 127 === DEL
+            // 255 === null byte
+            if (($ascii < 32 && $ascii !== 9)
+                || $ascii === 127
+                || $ascii > 254
+            ) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Assert a header value is valid.
+     *
+     * @param string $value
+     * @throws Exception\RuntimeException for invalid values
+     * @return void
+     */
+    public static function assertValid($value)
+    {
+        if (! self::isValid($value)) {
+            require_once 'Zend/Http/Header/Exception/InvalidArgumentException.php';
+            throw new Zend_Http_Header_Exception_InvalidArgumentException('Invalid header value');
+        }
+    }
+}

+ 8 - 0
library/Zend/Http/Header/SetCookie.php

@@ -32,6 +32,11 @@ require_once "Zend/Http/Header/Exception/InvalidArgumentException.php";
 require_once "Zend/Http/Header/Exception/RuntimeException.php";
 
 /**
+ * @see Zend_Http_Header_HeaderValue
+ */
+require_once "Zend/Http/Header/HeaderValue.php";
+
+/**
  * Zend_Http_Client is an implementation of an HTTP client in PHP. The client
  * supports basic features like sending different HTTP requests and handling
  * redirections, as well as more advanced features like proxy settings, HTTP
@@ -311,6 +316,7 @@ class Zend_Http_Header_SetCookie
      */
     public function setValue($value)
     {
+        Zend_Http_Header_HeaderValue::assertValid($value);
         $this->value = $value;
         return $this;
     }
@@ -405,6 +411,7 @@ class Zend_Http_Header_SetCookie
      */
     public function setDomain($domain)
     {
+        Zend_Http_Header_HeaderValue::assertValid($domain);
         $this->domain = $domain;
         return $this;
     }
@@ -422,6 +429,7 @@ class Zend_Http_Header_SetCookie
      */
     public function setPath($path)
     {
+        Zend_Http_Header_HeaderValue::assertValid($path);
         $this->path = $path;
         return $this;
     }

+ 54 - 18
library/Zend/Http/Response.php

@@ -22,6 +22,11 @@
  */
 
 /**
+ * @see Zend_Http_Header_HeaderValue
+ */
+require_once 'Zend/Http/Header/HeaderValue.php';
+
+/**
  * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
  * includes easy access to all the response's different elemts, as well as some
  * convenience methods for parsing and validating HTTP responses.
@@ -394,7 +399,7 @@ class Zend_Http_Response
      * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
      * @return string
      */
-    public function asString($br = "\n")
+    public function asString($br = "\r\n")
     {
         return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
     }
@@ -496,24 +501,35 @@ class Zend_Http_Response
     {
         $headers = array();
 
-        // First, split body and headers
-        $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
-        if (! $parts[0]) return $headers;
+        // First, split body and headers. Headers are separated from the
+        // message at exactly the sequence "\r\n\r\n"
+        $parts = preg_split('|(?:\r\n){2}|m', $response_str, 2);
+        if (! $parts[0]) {
+            return $headers;
+        }
 
-        // Split headers part to lines
-        $lines = explode("\n", $parts[0]);
+        // Split headers part to lines; "\r\n" is the only valid line separator.
+        $lines = explode("\r\n", $parts[0]);
         unset($parts);
         $last_header = null;
 
-        foreach($lines as $line) {
-            $line = trim($line, "\r\n");
-            if ($line == "") break;
+        foreach($lines as $index => $line) {
+            if ($index === 0 && preg_match('#^HTTP/\d+(?:\.\d+) [1-5]\d+#', $line)) {
+                // Status line; ignore
+                continue;
+            }
+
+            if ($line == "") {
+                // Done processing headers
+                break;
+            }
 
             // Locate headers like 'Location: ...' and 'Location:...' (note the missing space)
-            if (preg_match("|^([\w-]+):\s*(.+)|", $line, $m)) {
+            if (preg_match("|^([\w-]+):\s*(.+)|s", $line, $m)) {
                 unset($last_header);
-                $h_name = strtolower($m[1]);
+                $h_name  = strtolower($m[1]);
                 $h_value = $m[2];
+                Zend_Http_Header_HeaderValue::assertValid($h_value);
 
                 if (isset($headers[$h_name])) {
                     if (! is_array($headers[$h_name])) {
@@ -521,19 +537,39 @@ class Zend_Http_Response
                     }
 
                     $headers[$h_name][] = $h_value;
-                } else {
-                    $headers[$h_name] = $h_value;
+                    $last_header = $h_name;
+                    continue;
                 }
+
+                $headers[$h_name] = $h_value;
                 $last_header = $h_name;
-            } elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) {
+                continue;
+            }
+
+            // Identify header continuations
+            if (preg_match("|^[ \t](.+)$|s", $line, $m) && $last_header !== null) {
+                $h_value = trim($m[1]);
                 if (is_array($headers[$last_header])) {
                     end($headers[$last_header]);
                     $last_header_key = key($headers[$last_header]);
-                    $headers[$last_header][$last_header_key] .= $m[1];
-                } else {
-                    $headers[$last_header] .= $m[1];
+
+                    $h_value = $headers[$last_header][$last_header_key] . $h_value;
+                    Zend_Http_Header_HeaderValue::assertValid($h_value);
+
+                    $headers[$last_header][$last_header_key] = $h_value;
+                    continue;
                 }
+
+                $h_value = $headers[$last_header] . $h_value;
+                Zend_Http_Header_HeaderValue::assertValid($h_value);
+
+                $headers[$last_header] = $h_value;
+                continue;
             }
+
+            // Anything else is an error condition
+            require_once 'Zend/Http/Exception.php';
+            throw new Zend_Http_Exception('Invalid header line detected');
         }
 
         return $headers;
@@ -547,7 +583,7 @@ class Zend_Http_Response
      */
     public static function extractBody($response_str)
     {
-        $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
+        $parts = preg_split('|(?:\r\n){2}|m', $response_str, 2);
         if (isset($parts[1])) {
             return $parts[1];
         }

+ 92 - 0
library/Zend/Mail/Header/HeaderName.php

@@ -0,0 +1,92 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Mail
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Mail
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+final class Zend_Mail_Header_HeaderName
+{
+    /**
+     * No public constructor.
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * Filter the header name according to RFC 2822
+     *
+     * @see    http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2)
+     * @param  string $name
+     * @return string
+     */
+    public static function filter($name)
+    {
+        $result = '';
+        $tot    = strlen($name);
+        for ($i = 0; $i < $tot; $i += 1) {
+            $ord = ord($name[$i]);
+            if ($ord > 32 && $ord < 127 && $ord !== 58) {
+                $result .= $name[$i];
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Determine if the header name contains any invalid characters.
+     *
+     * @param string $name
+     * @return bool
+     */
+    public static function isValid($name)
+    {
+        $tot = strlen($name);
+        for ($i = 0; $i < $tot; $i += 1) {
+            $ord = ord($name[$i]);
+            if ($ord < 33 || $ord > 126 || $ord === 58) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Assert that the header name is valid.
+     *
+     * Raises an exception if invalid.
+     *
+     * @param string $name
+     * @throws Exception\RuntimeException
+     * @return void
+     */
+    public static function assertValid($name)
+    {
+        if (! self::isValid($name)) {
+            require_once 'Zend/Mail/Exception.php';
+            throw new Zend_Mail_Exception('Invalid header name detected');
+        }
+    }
+}

+ 136 - 0
library/Zend/Mail/Header/HeaderValue.php

@@ -0,0 +1,136 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Mail
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Mail
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+final class Zend_Mail_Header_HeaderValue
+{
+    /**
+     * No public constructor.
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * Filter the header value according to RFC 2822
+     *
+     * @see    http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2)
+     * @param  string $value
+     * @return string
+     */
+    public static function filter($value)
+    {
+        $result = '';
+        $tot    = strlen($value);
+
+        // Filter for CR and LF characters, leaving CRLF + WSP sequences for
+        // Long Header Fields (section 2.2.3 of RFC 2822)
+        for ($i = 0; $i < $tot; $i += 1) {
+            $ord = ord($value[$i]);
+            if (($ord < 32 || $ord > 126)
+                && $ord !== 13
+            ) {
+                continue;
+            }
+
+            if ($ord === 13) {
+                if ($i + 2 >= $tot) {
+                    continue;
+                }
+
+                $lf = ord($value[$i + 1]);
+                $sp = ord($value[$i + 2]);
+
+                if ($lf !== 10 || $sp !== 32) {
+                    continue;
+                }
+
+                $result .= "\r\n ";
+                $i += 2;
+                continue;
+            }
+
+            $result .= $value[$i];
+        }
+
+        return $result;
+    }
+
+    /**
+     * Determine if the header value contains any invalid characters.
+     *
+     * @see    http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2)
+     * @param string $value
+     * @return bool
+     */
+    public static function isValid($value)
+    {
+        $tot = strlen($value);
+        for ($i = 0; $i < $tot; $i += 1) {
+            $ord = ord($value[$i]);
+            if (($ord < 32 || $ord > 126)
+                && $ord !== 13
+            ) {
+                return false;
+            }
+
+            if ($ord === 13) {
+                if ($i + 2 >= $tot) {
+                    return false;
+                }
+
+                $lf = ord($value[$i + 1]);
+                $sp = ord($value[$i + 2]);
+
+                if ($lf !== 10 || $sp !== 32) {
+                    return false;
+                }
+
+                $i += 2;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Assert that the header value is valid.
+     *
+     * Raises an exception if invalid.
+     *
+     * @param string $value
+     * @throws Exception\RuntimeException
+     * @return void
+     */
+    public static function assertValid($value)
+    {
+        if (! self::isValid($value)) {
+            require_once 'Zend/Mail/Exception.php';
+            throw new Zend_Mail_Exception('Invalid header value detected');
+        }
+    }
+}

+ 1 - 0
library/Zend/Mail/Message.php

@@ -69,6 +69,7 @@ class Zend_Mail_Message extends Zend_Mail_Part implements Zend_Mail_Message_Inte
             } else {
                 $params['raw'] = stream_get_contents($params['file']);
             }
+            $params['raw'] = preg_replace("/(?<!\r)\n/", "\r\n", $params['raw']);
         }
 
         if (!empty($params['flags'])) {

+ 37 - 3
library/Zend/Mail/Part.php

@@ -26,6 +26,16 @@
 require_once 'Zend/Mime/Decode.php';
 
 /**
+ * @see Zend_Mail_Header_HeaderName
+ */
+require_once 'Zend/Mail/Header/HeaderName.php';
+
+/**
+ * @see Zend_Mail_Header_HeaderValue
+ */
+require_once 'Zend/Mail/Header/HeaderValue.php';
+
+/**
  * @see Zend_Mail_Part_Interface
  */
 require_once 'Zend/Mail/Part/Interface.php';
@@ -134,17 +144,19 @@ class Zend_Mail_Part implements RecursiveIterator, Zend_Mail_Part_Interface
         }
 
         if (isset($params['raw'])) {
-            Zend_Mime_Decode::splitMessage($params['raw'], $this->_headers, $this->_content);
+            Zend_Mime_Decode::splitMessage($params['raw'], $this->_headers, $this->_content, "\r\n");
         } else if (isset($params['headers'])) {
             if (is_array($params['headers'])) {
                 $this->_headers = $params['headers'];
+                $this->_validateHeaders($this->_headers);
             } else {
                 if (!empty($params['noToplines'])) {
-                    Zend_Mime_Decode::splitMessage($params['headers'], $this->_headers, $null);
+                    Zend_Mime_Decode::splitMessage($params['headers'], $this->_headers, $null, "\r\n");
                 } else {
-                    Zend_Mime_Decode::splitMessage($params['headers'], $this->_headers, $this->_topLines);
+                    Zend_Mime_Decode::splitMessage($params['headers'], $this->_headers, $this->_topLines, "\r\n");
                 }
             }
+
             if (isset($params['content'])) {
                 $this->_content = $params['content'];
             }
@@ -566,4 +578,26 @@ class Zend_Mail_Part implements RecursiveIterator, Zend_Mail_Part_Interface
         $this->countParts();
         $this->_iterationPos = 1;
     }
+
+    /**
+     * Ensure headers do not contain invalid characters
+     *
+     * @param array $headers
+     * @param bool $assertNames
+     */
+    protected function _validateHeaders(array $headers, $assertNames = true)
+    {
+        foreach ($headers as $name => $value) {
+            if ($assertNames) {
+                Zend_Mail_Header_HeaderName::assertValid($name);
+            }
+
+            if (is_array($value)) {
+                $this->_validateHeaders($value, false);
+                continue;
+            }
+
+            Zend_Mail_Header_HeaderValue::assertValid($value);
+        }
+    }
 }

+ 2 - 0
tests/Zend/Http/Client/AllTests.php

@@ -24,6 +24,7 @@ if (!defined('PHPUnit_MAIN_METHOD')) {
     define('PHPUnit_MAIN_METHOD', 'Zend_Http_Client_AllTests::main');
 }
 
+require_once 'Zend/Http/Client/ClientTest.php';
 require_once 'Zend/Http/Client/StaticTest.php';
 require_once 'Zend/Http/Client/SocketTest.php';
 require_once 'Zend/Http/Client/SocketKeepaliveTest.php';
@@ -53,6 +54,7 @@ class Zend_Http_Client_AllTests
     {
         $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Http_Client');
 
+        $suite->addTestSuite('Zend_Http_Client_ClientTest');
         $suite->addTestSuite('Zend_Http_Client_StaticTest');
         if (defined('TESTS_ZEND_HTTP_CLIENT_BASEURI') && Zend_Uri_Http::check(TESTS_ZEND_HTTP_CLIENT_BASEURI)) {
             $suite->addTestSuite('Zend_Http_Client_SocketTest');

+ 71 - 0
tests/Zend/Http/Client/ClientTest.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+require_once 'Zend/Http/Client.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Http_Client
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @group      Zend_Http
+ * @group      Zend_Http_Client
+ */
+class Zend_Http_Client_ClientTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Set up the test case
+     *
+     */
+    protected function setUp()
+    {
+        $this->client = new Zend_Http_Client();
+    }
+
+    public function invalidHeaders()
+    {
+        return array(
+            'invalid-name-cr'                      => array("X-Foo-\rBar", 'value'),
+            'invalid-name-lf'                      => array("X-Foo-\nBar", 'value'),
+            'invalid-name-crlf'                    => array("X-Foo-\r\nBar", 'value'),
+            'invalid-value-cr'                     => array('X-Foo-Bar', "value\risEvil"),
+            'invalid-value-lf'                     => array('X-Foo-Bar', "value\nisEvil"),
+            'invalid-value-bad-continuation'       => array('X-Foo-Bar', "value\r\nisEvil"),
+            'invalid-array-value-cr'               => array('X-Foo-Bar', array("value\risEvil")),
+            'invalid-array-value-lf'               => array('X-Foo-Bar', array("value\nisEvil")),
+            'invalid-array-value-bad-continuation' => array('X-Foo-Bar', array("value\r\nisEvil")),
+        );
+    }
+
+    /**
+     * @dataProvider invalidHeaders
+     * @group ZF2015-04
+     */
+    public function testHeadersContainingCRLFInjectionRaiseAnException($name, $value)
+    {
+        $this->setExpectedException('Zend_Http_Exception');
+        $this->client->setHeaders(array(
+            $name => $value,
+        ));
+    }
+}

+ 0 - 1
tests/Zend/Http/Client/CommonHttpTests.php

@@ -1359,5 +1359,4 @@ abstract class Zend_Http_Client_CommonHttpTests extends PHPUnit_Framework_TestCa
             array(55)
         );
     }
-
 }

+ 6 - 0
tests/Zend/Http/Header/AllTests.php

@@ -25,6 +25,11 @@ if (!defined('PHPUnit_MAIN_METHOD')) {
 }
 
 /**
+ * @see Zend_Http_Header_HeaderValue
+ */
+require_once 'Zend/Http/Header/HeaderValueTest.php';
+
+/**
  * @see Zend_Http_Header_SetCookie
  */
 require_once 'Zend/Http/Header/SetCookieTest.php';
@@ -49,6 +54,7 @@ class Zend_Http_Header_AllTests
     {
         $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Http - Header');
 
+        $suite->addTestSuite('Zend_Http_Header_HeaderValueTest');
         $suite->addTestSuite('Zend_Http_Header_SetCookieTest');
 
         return $suite;

+ 116 - 0
tests/Zend/Http/Header/HeaderValueTest.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Header
+ * @version    $Id$
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Http/Header/HeaderValue.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Http_Cookie
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @group      Zend_Http
+ * @group      Zend_Http_Header
+ */
+class Zend_Http_Header_HeaderValueTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Data for filter value
+     */
+    public function getFilterValues()
+    {
+        return array(
+            array("This is a\n test", "This is a test"),
+            array("This is a\r test", "This is a test"),
+            array("This is a\n\r test", "This is a test"),
+            array("This is a\r\n  test", "This is a  test"),
+            array("This is a \r\ntest", "This is a test"),
+            array("This is a \r\n\n test", "This is a  test"),
+            array("This is a\n\n test", "This is a test"),
+            array("This is a\r\r test", "This is a test"),
+            array("This is a \r\r\n test", "This is a  test"),
+            array("This is a \r\n\r\ntest", "This is a test"),
+            array("This is a \r\n\n\r\n test", "This is a  test")
+        );
+    }
+
+    /**
+     * @dataProvider getFilterValues
+     * @group ZF2015-04
+     */
+    public function testFiltersValuesPerRfc7230($value, $expected)
+    {
+        $this->assertEquals($expected, Zend_Http_Header_HeaderValue::filter($value));
+    }
+
+    public function validateValues()
+    {
+        return array(
+            array("This is a\n test", 'assertFalse'),
+            array("This is a\r test", 'assertFalse'),
+            array("This is a\n\r test", 'assertFalse'),
+            array("This is a\r\n  test", 'assertFalse'),
+            array("This is a \r\ntest", 'assertFalse'),
+            array("This is a \r\n\n test", 'assertFalse'),
+            array("This is a\n\n test", 'assertFalse'),
+            array("This is a\r\r test", 'assertFalse'),
+            array("This is a \r\r\n test", 'assertFalse'),
+            array("This is a \r\n\r\ntest", 'assertFalse'),
+            array("This is a \r\n\n\r\n test", 'assertFalse')
+        );
+    }
+
+    /**
+     * @dataProvider validateValues
+     * @group ZF2015-04
+     */
+    public function testValidatesValuesPerRfc7230($value, $assertion)
+    {
+        $this->{$assertion}(Zend_Http_Header_HeaderValue::isValid($value));
+    }
+
+    public function assertValues()
+    {
+        return array(
+            array("This is a\n test"),
+            array("This is a\r test"),
+            array("This is a\n\r test"),
+            array("This is a \r\ntest"),
+            array("This is a \r\n\n test"),
+            array("This is a\n\n test"),
+            array("This is a\r\r test"),
+            array("This is a \r\r\n test"),
+            array("This is a \r\n\r\ntest"),
+            array("This is a \r\n\n\r\n test")
+        );
+    }
+
+    /**
+     * @dataProvider assertValues
+     * @group ZF2015-04
+     */
+    public function testAssertValidRaisesExceptionForInvalidValue($value)
+    {
+        $this->setExpectedException('Zend_Http_Header_Exception_InvalidArgumentException');
+        Zend_Http_Header_HeaderValue::assertValid($value);
+    }
+}

+ 21 - 1
tests/Zend/Http/Header/SetCookieTest.php

@@ -371,5 +371,25 @@ class Zend_Http_Header_SetCookieTest extends PHPUnit_Framework_TestCase
             ),
         );
     }
-}
 
+    public function invalidCookieComponentValues()
+    {
+        return array(
+            'setName'   => array('setName', "This\r\nis\nan\revil\r\n\r\nvalue"),
+            'setValue'  => array('setValue', "This\r\nis\nan\revil\r\n\r\nvalue"),
+            'setDomain' => array('setDomain', "This\r\nis\nan\revil\r\n\r\nvalue"),
+            'setPath'   => array('setPath', "This\r\nis\nan\revil\r\n\r\nvalue"),
+        );
+    }
+
+    /**
+     * @group ZF2015-04
+     * @dataProvider invalidCookieComponentValues
+     */
+    public function testDoesNotAllowCRLFAttackVectorsViaSetters($setter, $value)
+    {
+        $cookie = new Zend_Http_Header_SetCookie();
+        $this->setExpectedException('Zend_Http_Header_Exception_InvalidArgumentException');
+        $cookie->{$setter}($value);
+    }
+}

+ 47 - 19
tests/Zend/Http/ResponseTest.php

@@ -64,7 +64,7 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * Make sure wer can handle non-RFC complient "deflate" responses.
+     * Make sure we can handle non-RFC complient "deflate" responses.
      *
      * Unlike stanrdard 'deflate' response, those do not contain the zlib header
      * and trailer. Unfortunately some buggy servers (read: IIS) send those and
@@ -76,6 +76,16 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
     {
         $response_text = file_get_contents(dirname(__FILE__) . '/_files/response_deflate_iis');
 
+        // Ensure headers are correctly formatted (i.e., separated with "\r\n" sequence)
+        //
+        // Line endings are an issue inside the canned response; the
+        // following uses a negative lookbehind assertion, and replaces any \n
+        // not preceded by \r with the sequence \r\n within the headers,
+        // ensuring that the message is well-formed.
+        list($headers, $message) = explode("\n\n", $response_text, 2);
+        $headers = preg_replace("#(?<!\r)\n#", "\r\n", $headers);
+        $response_text = $headers . "\r\n\r\n" . $message;
+
         $res = Zend_Http_Response::fromString($response_text);
 
         $this->assertEquals('deflate', $res->getHeader('Content-encoding'));
@@ -105,19 +115,6 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
         $this->assertEquals('c0cc9d44790fa2a58078059bab1902a9', md5($res->getRawBody()));
     }
 
-
-    public function testLineBreaksCompatibility()
-    {
-        $response_text_lf = $this->readResponse('response_lfonly');
-        $res_lf = Zend_Http_Response::fromString($response_text_lf);
-
-        $response_text_crlf = $this->readResponse('response_crlf');
-        $res_crlf = Zend_Http_Response::fromString($response_text_crlf);
-
-        $this->assertEquals($res_lf->getHeadersAsString(true), $res_crlf->getHeadersAsString(true), 'Responses headers do not match');
-        $this->assertEquals($res_lf->getBody(), $res_crlf->getBody(), 'Response bodies do not match');
-    }
-
     public function testExtractMessageCrlf()
     {
         $response_text = file_get_contents(dirname(__FILE__) . '/_files/response_crlf');
@@ -209,8 +206,8 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
         $response_str = $this->readResponse('response_404');
         $response = Zend_Http_Response::fromString($response_str);
 
-        $this->assertEquals(strtolower($response_str), strtolower($response->asString()), 'Response convertion to string does not match original string');
-        $this->assertEquals(strtolower($response_str), strtolower((string)$response), 'Response convertion to string does not match original string');
+        $this->assertEquals(strtolower($response_str), strtolower($response->asString()), 'Response conversion to string does not match original string');
+        $this->assertEquals(strtolower($response_str), strtolower((string) $response), 'Response conversion to string does not match original string');
     }
 
     public function testGetHeaders()
@@ -307,7 +304,8 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
      */
     public function testLeadingWhitespaceBody()
     {
-        $body = Zend_Http_Response::extractBody($this->readResponse('response_leadingws'));
+        $message = file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'response_leadingws');
+        $body    = Zend_Http_Response::extractBody($message);
         $this->assertEquals($body, "\r\n\t  \n\r\tx", 'Extracted body is not identical to expected body');
     }
 
@@ -320,7 +318,7 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
      */
     public function testMultibyteChunkedResponse()
     {
-        $md5 = 'ab952f1617d0e28724932401f2d3c6ae';
+        $md5 = 'f734924685f92b243c8580848cadc560';
 
         $response = Zend_Http_Response::fromString($this->readResponse('response_multibyte_body'));
         $this->assertEquals($md5, md5($response->getBody()));
@@ -385,6 +383,36 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
      */
     protected function readResponse($response)
     {
-        return file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . $response);
+        $message = file_get_contents(
+            dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . $response
+        );
+        // Line endings are sometimes an issue inside the canned responses; the
+        // following is a negative lookbehind assertion, and replaces any \n
+        // not preceded by \r with the sequence \r\n, ensuring that the message
+        // is well-formed.
+        return preg_replace("#(?<!\r)\n#", "\r\n", $message);
+    }
+
+    public function invalidResponseHeaders()
+    {
+        return array(
+            'bad-status-line'            => array("HTTP/1.0a 200 OK\r\nHost: example.com\r\n\r\nMessage Body"),
+            'nl-in-header'               => array("HTTP/1.1 200 OK\r\nHost: example.\ncom\r\n\r\nMessage Body"),
+            'cr-in-header'               => array("HTTP/1.1 200 OK\r\nHost: example.\rcom\r\n\r\nMessage Body"),
+            'bad-continuation'           => array("HTTP/1.1 200 OK\r\nHost: example.\r\ncom\r\n\r\nMessage Body"),
+            'no-status-nl-in-header'     => array("Host: example.\ncom\r\n\r\nMessage Body"),
+            'no-status-cr-in-header'     => array("Host: example.\rcom\r\n\r\nMessage Body"),
+            'no-status-bad-continuation' => array("Host: example.\r\ncom\r\n\r\nMessage Body"),
+        );
+    }
+
+    /**
+     * @group ZF2015-04
+     * @dataProvider invalidResponseHeaders
+     */
+    public function testExtractHeadersRaisesExceptionWhenDetectingCRLFInjection($message)
+    {
+        $this->setExpectedException('Zend_Http_Exception', 'Invalid');
+        Zend_Http_Response::extractHeaders($message);
     }
 }

+ 2 - 0
tests/Zend/Mail/AllTests.php

@@ -24,6 +24,7 @@ if (!defined('PHPUnit_MAIN_METHOD')) {
     define('PHPUnit_MAIN_METHOD', 'Zend_Mail_AllTests::main');
 }
 
+require_once 'Zend/Mail/Header/AllTests.php';
 require_once 'Zend/Mail/MailTest.php';
 require_once 'Zend/Mail/MboxTest.php';
 require_once 'Zend/Mail/MboxMessageOldTest.php';
@@ -60,6 +61,7 @@ class Zend_Mail_AllTests
     {
         $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Mail');
 
+        $suite->addTest(Zend_Mail_Header_AllTests::suite());
         $suite->addTestSuite('Zend_Mail_MailTest');
         $suite->addTestSuite('Zend_Mail_MessageTest');
         $suite->addTestSuite('Zend_Mail_InterfaceTest');

+ 58 - 0
tests/Zend/Mail/Header/AllTests.php

@@ -0,0 +1,58 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Mail
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Mail_Header_AllTests::main');
+}
+
+require_once 'Zend/Mail/Header/HeaderNameTest.php';
+require_once 'Zend/Mail/Header/HeaderValueTest.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Mail
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @group      Zend_Mail
+ */
+class Zend_Mail_Header_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Mail_Header');
+
+        $suite->addTestSuite('Zend_Mail_Header_HeaderNameTest');
+        $suite->addTestSuite('Zend_Mail_Header_HeaderValueTest');
+
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Mail_Header_AllTests::main') {
+    Zend_Mail_Header_AllTests::main();
+}

+ 96 - 0
tests/Zend/Mail/Header/HeaderNameTest.php

@@ -0,0 +1,96 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Mail
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Mail_Message
+ */
+require_once 'Zend/Mail/Header/HeaderName.php';
+
+class Zend_Mail_Header_HeaderNameTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Data for filter name
+     */
+    public function getFilterNames()
+    {
+        return array(
+            array('Subject', 'Subject'),
+            array('Subject:', 'Subject'),
+            array(':Subject:', 'Subject'),
+            array('Subject' . chr(32), 'Subject'),
+            array('Subject' . chr(33), 'Subject' . chr(33)),
+            array('Subject' . chr(126), 'Subject' . chr(126)),
+            array('Subject' . chr(127), 'Subject'),
+        );
+    }
+
+    /**
+     * @dataProvider getFilterNames
+     * @group ZF2015-04
+     */
+    public function testFilterName($name, $expected)
+    {
+        $this->assertEquals($expected, Zend_Mail_Header_HeaderName::filter($name));
+    }
+
+    public function validateNames()
+    {
+        return array(
+            array('Subject', 'assertTrue'),
+            array('Subject:', 'assertFalse'),
+            array(':Subject:', 'assertFalse'),
+            array('Subject' . chr(32), 'assertFalse'),
+            array('Subject' . chr(33), 'assertTrue'),
+            array('Subject' . chr(126), 'assertTrue'),
+            array('Subject' . chr(127), 'assertFalse'),
+        );
+    }
+
+    /**
+     * @dataProvider validateNames
+     * @group ZF2015-04
+     */
+    public function testValidateName($name, $assertion)
+    {
+        $this->{$assertion}(Zend_Mail_Header_HeaderName::isValid($name));
+    }
+
+    public function assertNames()
+    {
+        return array(
+            array('Subject:'),
+            array(':Subject:'),
+            array('Subject' . chr(32)),
+            array('Subject' . chr(127)),
+        );
+    }
+
+    /**
+     * @dataProvider assertNames
+     * @group ZF2015-04
+     */
+    public function testAssertValidRaisesExceptionForInvalidNames($name)
+    {
+        $this->setExpectedException('Zend_Mail_Exception', 'Invalid');
+        Zend_Mail_Header_HeaderName::assertValid($name);
+    }
+}

+ 110 - 0
tests/Zend/Mail/Header/HeaderValueTest.php

@@ -0,0 +1,110 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Mail
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Mail_Message
+ */
+require_once 'Zend/Mail/Header/HeaderValue.php';
+
+class Zend_Mail_Header_HeaderValueTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Data for filter value
+     */
+    public function getFilterValues()
+    {
+        return array(
+            array("This is a\n test", "This is a test"),
+            array("This is a\r test", "This is a test"),
+            array("This is a\n\r test", "This is a test"),
+            array("This is a\r\n  test", "This is a\r\n  test"),
+            array("This is a \r\ntest", "This is a test"),
+            array("This is a \r\n\n test", "This is a  test"),
+            array("This is a\n\n test", "This is a test"),
+            array("This is a\r\r test", "This is a test"),
+            array("This is a \r\r\n test", "This is a \r\n test"),
+            array("This is a \r\n\r\ntest", "This is a test"),
+            array("This is a \r\n\n\r\n test", "This is a \r\n test")
+        );
+    }
+
+    /**
+     * @dataProvider getFilterValues
+     * @group ZF2015-04
+     */
+    public function testFilterValue($value, $expected)
+    {
+        $this->assertEquals($expected, Zend_Mail_Header_HeaderValue::filter($value));
+    }
+
+    public function validateValues()
+    {
+        return array(
+            array("This is a\n test", 'assertFalse'),
+            array("This is a\r test", 'assertFalse'),
+            array("This is a\n\r test", 'assertFalse'),
+            array("This is a\r\n  test", 'assertTrue'),
+            array("This is a \r\ntest", 'assertFalse'),
+            array("This is a \r\n\n test", 'assertFalse'),
+            array("This is a\n\n test", 'assertFalse'),
+            array("This is a\r\r test", 'assertFalse'),
+            array("This is a \r\r\n test", 'assertFalse'),
+            array("This is a \r\n\r\ntest", 'assertFalse'),
+            array("This is a \r\n\n\r\n test", 'assertFalse')
+        );
+    }
+
+    /**
+     * @dataProvider validateValues
+     * @group ZF2015-04
+     */
+    public function testValidateValue($value, $assertion)
+    {
+        $this->{$assertion}(Zend_Mail_Header_HeaderValue::isValid($value));
+    }
+
+    public function assertValues()
+    {
+        return array(
+            array("This is a\n test"),
+            array("This is a\r test"),
+            array("This is a\n\r test"),
+            array("This is a \r\ntest"),
+            array("This is a \r\n\n test"),
+            array("This is a\n\n test"),
+            array("This is a\r\r test"),
+            array("This is a \r\r\n test"),
+            array("This is a \r\n\r\ntest"),
+            array("This is a \r\n\n\r\n test")
+        );
+    }
+
+    /**
+     * @dataProvider assertValues
+     * @group ZF2015-04
+     */
+    public function testAssertValidRaisesExceptionForInvalidValues($value)
+    {
+        $this->setExpectedException('Zend_Mail_Exception', 'Invalid');
+        Zend_Mail_Header_HeaderValue::assertValid($value);
+    }
+}

+ 32 - 1
tests/Zend/Mail/MessageTest.php

@@ -49,7 +49,17 @@ class Zend_Mail_MessageTest extends PHPUnit_Framework_TestCase
 
     public function setUp()
     {
-        $this->_file = dirname(__FILE__) . '/_files/mail.txt';
+        $this->_file = tempnam(sys_get_temp_dir(), 'zm_');
+        $mail = file_get_contents(dirname(__FILE__) . '/_files/mail.txt');
+        $mail = preg_replace("/(?<!\r)\n/", "\r\n", $mail);
+        file_put_contents($this->_file, $mail);
+    }
+
+    public function tearDown()
+    {
+        if (file_exists($this->_file)) {
+            unlink($this->_file);
+        }
     }
 
     public function testInvalidFile()
@@ -510,7 +520,28 @@ class Zend_Mail_MessageTest extends PHPUnit_Framework_TestCase
             $this->assertEquals('ZF3745_Mail_Part', get_class($part));
         }
     }
+
+    public function invalidHeaders()
+    {
+        return array(
+            'name'        => array("Fake\r\n\r\rnevilContent", 'value'),
+            'value'       => array('Fake', "foo-bar\r\n\r\nevilContent"),
+            'multi-value' => array('Fake', array('okay', "foo-bar\r\n\r\nevilContent")),
+        );
+    }
     
+    /**
+     * @dataProvider invalidHeaders
+     * @group ZF2015-04
+     */
+    public function testRaisesExceptionWhenProvidedWithHeaderContainingCRLFInjection($name, $value)
+    {
+        $headers = array($name => $value);
+        $this->setExpectedException('Zend_Mail_Exception', 'valid');
+        $message = new Zend_Mail_Message(array(
+            'headers' => $headers,
+        ));
+    }
 }
 
 /**